├── .clang-format
├── .clang-tidy
├── .commitlintrc.yml
├── .github
└── workflows
│ ├── build.yml
│ └── lint.yml
├── .gitignore
├── .versionrc
├── .vscode
├── c_cpp_properties.json
├── launch.json
├── settings.json
└── tasks.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
└── images
│ ├── screenshot-1.0.0.png
│ └── screenshot-beta.png
├── example
└── config
├── include
├── algebra.h
├── application.h
├── box.h
├── clipboard.h
├── config.h
├── file.h
├── paint.h
├── pixbuf.h
├── render.h
├── swappy.h
└── util.h
├── meson.build
├── meson_options.txt
├── package.json
├── res
├── icons
│ └── hicolor
│ │ └── scalable
│ │ └── apps
│ │ └── swappy.svg
├── meson.build
├── style
│ └── swappy.css
├── swappy.glade
└── swappy.gresource.xml
├── script
├── bump-meson-build.js
└── github-release
├── src
├── algebra.c
├── application.c
├── box.c
├── clipboard.c
├── config.c
├── file.c
├── main.c
├── paint.c
├── pixbuf.c
├── po
│ ├── LINGUAS
│ ├── POTFILES
│ ├── de.po
│ ├── en.po
│ ├── fr.po
│ ├── meson.build
│ ├── pt_BR.po
│ ├── swappy.desktop.in
│ ├── swappy.pot
│ ├── tr.po
│ └── zh_CN.po
├── render.c
└── util.c
├── swappy.1.scd
└── test
└── images
├── heart-transparent.png
├── large.png
├── passwords.png
└── small-blue.png
/.clang-format:
--------------------------------------------------------------------------------
1 | # Use the Google style in this project.
2 | BasedOnStyle: Google
3 | IndentWidth: 2
4 |
5 | DerivePointerAlignment: false
6 | PointerAlignment: Right
7 |
--------------------------------------------------------------------------------
/.clang-tidy:
--------------------------------------------------------------------------------
1 | ---
2 | Checks: 'clang-diagnostic-*,clang-analyzer-*,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling'
3 | WarningsAsErrors: ''
4 | HeaderFilterRegex: ''
5 | FormatStyle: none
6 | CheckOptions:
7 | - key: cert-dcl16-c.NewSuffixes
8 | value: 'L;LL;LU;LLU'
9 | - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField
10 | value: '0'
11 | - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors
12 | value: '1'
13 | - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
14 | value: '1'
15 | - key: google-readability-braces-around-statements.ShortStatementLines
16 | value: '1'
17 | - key: google-readability-function-size.StatementThreshold
18 | value: '800'
19 | - key: google-readability-namespace-comments.ShortNamespaceLines
20 | value: '10'
21 | - key: google-readability-namespace-comments.SpacesBeforeComments
22 | value: '2'
23 | - key: modernize-loop-convert.MaxCopySize
24 | value: '16'
25 | - key: modernize-loop-convert.MinConfidence
26 | value: reasonable
27 | - key: modernize-loop-convert.NamingStyle
28 | value: CamelCase
29 | - key: modernize-pass-by-value.IncludeStyle
30 | value: llvm
31 | - key: modernize-replace-auto-ptr.IncludeStyle
32 | value: llvm
33 | - key: modernize-use-nullptr.NullMacros
34 | value: 'NULL'
35 | ...
36 |
37 |
--------------------------------------------------------------------------------
/.commitlintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends:
3 | - "@commitlint/config-conventional"
4 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build-and-lint:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: "Checkout repository"
10 | uses: actions/checkout@v4
11 |
12 | - name: Install dependencies
13 | run: |
14 | sudo apt-get update
15 | sudo apt --yes install libgtk-3-dev gettext meson ninja-build scdoc clang clang-format clang-tidy
16 |
17 | - name: "Build with gcc"
18 | run: |
19 | pkg-config --list-all
20 | CC=gcc meson setup build
21 | ninja -C build
22 |
23 | - name: "Build with clang"
24 | run: |
25 | git clean -fdx
26 | CC=clang meson setup build
27 | ninja -C build
28 |
29 | - name: "Lint with clang-format"
30 | run: |
31 | echo "Making sure clang-format is correct..."
32 | git ls-files -- '*.[ch]' | xargs clang-format -Werror -n
33 | echo "Running clang-tidy..."
34 | run-clang-tidy -p build
35 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Commitlint
2 | on: [pull_request]
3 |
4 | jobs:
5 | commitlint:
6 | runs-on: ubuntu-latest
7 | name: "Commitlint"
8 | steps:
9 | - name: "Checkout repository"
10 | uses: actions/checkout@v4
11 | with:
12 | fetch-depth: 0
13 |
14 | - name: "Commitlint"
15 | uses: wagoid/commitlint-github-action@v6
16 | with:
17 | configFile: "./.commitlintrc.yml"
18 | failOnWarnings: true
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Prerequisites
2 | *.d
3 |
4 | # Object files
5 | *.o
6 | *.ko
7 | *.obj
8 | *.elf
9 |
10 | # Linker output
11 | *.ilk
12 | *.map
13 | *.exp
14 |
15 | # Precompiled Headers
16 | *.gch
17 | *.pch
18 |
19 | # Libraries
20 | *.lib
21 | *.a
22 | *.la
23 | *.lo
24 |
25 | # Shared objects (inc. Windows DLLs)
26 | *.dll
27 | *.so
28 | *.so.*
29 | *.dylib
30 |
31 | # Executables
32 | *.exe
33 | *.out
34 | *.app
35 | *.i*86
36 | *.x86_64
37 | *.hex
38 |
39 | # Debug files
40 | *.dSYM/
41 | *.su
42 | *.idb
43 | *.pdb
44 |
45 | # Kernel Module Compile Results
46 | *.mod*
47 | *.cmd
48 | .tmp_versions/
49 | modules.order
50 | Module.symvers
51 | Mkfile.old
52 | dkms.conf
53 |
54 | # Temporary files
55 | *.ui~
56 |
57 | # Build folders
58 | build/
59 | release/
60 | .cache/
61 |
--------------------------------------------------------------------------------
/.versionrc:
--------------------------------------------------------------------------------
1 | {
2 | "bumpFiles": [
3 | {
4 | "filename": "meson.build",
5 | "updater": "script/bump-meson-build.js"
6 | },
7 | {
8 | "filename": "package.json"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/c_cpp_properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "name": "Linux",
5 | "defines": [],
6 | "compilerPath": "/usr/bin/clang",
7 | "cStandard": "c11",
8 | "cppStandard": "c++17",
9 | "intelliSenseMode": "clang-x64",
10 | "compileCommands": "${workspaceFolder}/build/compile_commands.json",
11 | "compilerArgs": ["build"],
12 | "includePath": [
13 | "/usr/include/cairo/",
14 | "/usr/include/gdk-pixbuf-2.0/",
15 | "/usr/include/gio-unix-2.0/",
16 | "/usr/include/glib-2.0/",
17 | "/usr/include/gtk-3.0/",
18 | "/usr/include/pango-1.0/",
19 | "/usr/lib/clang/11.0.1/include/"
20 | ]
21 | }
22 | ],
23 | "version": 4
24 | }
25 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "swappy - geometry",
9 | "type": "cppdbg",
10 | "request": "launch",
11 | "program": "${workspaceFolder}/build/swappy",
12 | "args": ["-g", "0,0 200x200"],
13 | "stopAtEntry": false,
14 | "cwd": "${workspaceFolder}",
15 | "environment": [
16 | {
17 | "name": "G_MESSAGES_DEBUG",
18 | "value": "all"
19 | }
20 | ],
21 | "externalConsole": false,
22 | "preLaunchTask": "build",
23 | "MIMode": "gdb",
24 | "setupCommands": [
25 | {
26 | "description": "Enable pretty-printing for gdb",
27 | "text": "-enable-pretty-printing",
28 | "ignoreFailures": true
29 | }
30 | ]
31 | },
32 | {
33 | "name": "swappy - file",
34 | "type": "cppdbg",
35 | "request": "launch",
36 | "program": "${workspaceFolder}/build/swappy",
37 | "args": ["-f", "docs/images/screenshot-beta.png"],
38 | "stopAtEntry": false,
39 | "cwd": "${workspaceFolder}",
40 | "environment": [
41 | {
42 | "name": "G_MESSAGES_DEBUG",
43 | "value": "all"
44 | }
45 | ],
46 | "externalConsole": false,
47 | "preLaunchTask": "build",
48 | "MIMode": "gdb",
49 | "setupCommands": [
50 | {
51 | "description": "Enable pretty-printing for gdb",
52 | "text": "-enable-pretty-printing",
53 | "ignoreFailures": true
54 | }
55 | ]
56 | },
57 | {
58 | "name": "swappy - file (small blue)",
59 | "type": "cppdbg",
60 | "request": "launch",
61 | "program": "${workspaceFolder}/build/swappy",
62 | "args": ["-f", "test/images/small-blue.png"],
63 | "stopAtEntry": false,
64 | "cwd": "${workspaceFolder}",
65 | "environment": [
66 | {
67 | "name": "G_MESSAGES_DEBUG",
68 | "value": "all"
69 | }
70 | ],
71 | "externalConsole": false,
72 | "preLaunchTask": "build",
73 | "MIMode": "gdb",
74 | "setupCommands": [
75 | {
76 | "description": "Enable pretty-printing for gdb",
77 | "text": "-enable-pretty-printing",
78 | "ignoreFailures": true
79 | }
80 | ]
81 | },
82 | {
83 | "name": "swappy - file (large)",
84 | "type": "cppdbg",
85 | "request": "launch",
86 | "program": "${workspaceFolder}/build/swappy",
87 | "args": ["-f", "test/images/large.png"],
88 | "stopAtEntry": false,
89 | "cwd": "${workspaceFolder}",
90 | "environment": [
91 | {
92 | "name": "G_MESSAGES_DEBUG",
93 | "value": "all"
94 | }
95 | ],
96 | "externalConsole": false,
97 | "preLaunchTask": "build",
98 | "MIMode": "gdb",
99 | "setupCommands": [
100 | {
101 | "description": "Enable pretty-printing for gdb",
102 | "text": "-enable-pretty-printing",
103 | "ignoreFailures": true
104 | }
105 | ]
106 | },
107 | {
108 | "name": "swappy - file & stdout",
109 | "type": "cppdbg",
110 | "request": "launch",
111 | "program": "${workspaceFolder}/build/swappy",
112 | "args": ["-f", "docs/images/screenshot-beta.png", "-o", "-"],
113 | "stopAtEntry": false,
114 | "cwd": "${workspaceFolder}",
115 | "environment": [
116 | {
117 | "name": "G_MESSAGES_DEBUG",
118 | "value": "all"
119 | }
120 | ],
121 | "externalConsole": false,
122 | "preLaunchTask": "build",
123 | "MIMode": "gdb",
124 | "setupCommands": [
125 | {
126 | "description": "Enable pretty-printing for gdb",
127 | "text": "-enable-pretty-printing",
128 | "ignoreFailures": true
129 | }
130 | ]
131 | },
132 | {
133 | "name": "swappy - stdin",
134 | "type": "cppdbg",
135 | "request": "launch",
136 | "program": "${workspaceFolder}/build/swappy",
137 | "args": ["-f", "-"],
138 | "stopAtEntry": false,
139 | "cwd": "${workspaceFolder}",
140 | "environment": [
141 | {
142 | "name": "G_MESSAGES_DEBUG",
143 | "value": "all"
144 | }
145 | ],
146 | "externalConsole": false,
147 | "preLaunchTask": "build",
148 | "MIMode": "gdb",
149 | "setupCommands": [
150 | {
151 | "description": "Enable pretty-printing for gdb",
152 | "text": "-enable-pretty-printing",
153 | "ignoreFailures": true
154 | }
155 | ]
156 | }
157 | ]
158 | }
159 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "mesonbuild.configureOnOpen": true,
3 | "files.associations": {
4 | "*.h": "c",
5 | "*.c": "c"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "type": "shell",
7 | "command": "ninja",
8 | "group": {
9 | "kind": "build",
10 | "isDefault": true
11 | },
12 | "options": {
13 | "cwd": "${workspaceFolder}/build"
14 | },
15 | "problemMatcher": {
16 | "base": "$gcc",
17 | "fileLocation": ["relative", "${workspaceFolder}/build"]
18 | }
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [1.8.0](https://github.com/jtheoof/swappy/compare/v1.7.1...v1.8.0) (2025-08-27)
6 |
7 |
8 | ### Features
9 |
10 | * **config:** add left hand friendly keybinds ([e3b8bf6](https://github.com/jtheoof/swappy/commit/e3b8bf610fa64d21e053a8e25fb74acda334ba3d))
11 | * **ui:** add transparency feature ([0416812](https://github.com/jtheoof/swappy/commit/0416812d63453d3f23e1e1e354882032ef86590b))
12 | * **ui:** support input method when edit text ([5e3808c](https://github.com/jtheoof/swappy/commit/5e3808c7a3631c948c701a34de455cfaa3550757))
13 | * **ui:** text supports pasting using Ctrl-v ([59dca02](https://github.com/jtheoof/swappy/commit/59dca0244906cf276a69ed11f21b5ed635690766))
14 |
15 |
16 | ### Bug Fixes
17 |
18 | * **application:** bring back output option ([061155c](https://github.com/jtheoof/swappy/commit/061155c9dee96c6dc4e7c3a4b4dc03d45e7fe192)), closes [#202](https://github.com/jtheoof/swappy/issues/202)
19 | * **application:** save twice if auto_save and -o option ([e5cde68](https://github.com/jtheoof/swappy/commit/e5cde680e6c5180e35c80c0868effda1b5b8b1d9))
20 |
21 | ### [1.7.1](https://github.com/jtheoof/swappy/compare/v1.7.0...v1.7.1) (2025-08-18)
22 |
23 |
24 | ### Bug Fixes
25 |
26 | * **render:** fix drawing transparent images ([49f84fd](https://github.com/jtheoof/swappy/commit/49f84fd681bc7cb707796e19086a51f7668ee228))
27 |
28 | ## [1.7.0](https://github.com/jtheoof/swappy/compare/v1.5.1...v1.7.0) (2025-08-18)
29 |
30 |
31 | ### Features
32 |
33 | * auto saving on quit app ([72a511c](https://github.com/jtheoof/swappy/commit/72a511c435e9e605b8269669df02996c6b2b5e10))
34 | * **config:** custom_color config option ([726159d](https://github.com/jtheoof/swappy/commit/726159d81b073f13c6e7661392c0440d4af162bf))
35 | * **i18n:** add zh_CN locale ([9e7f3b2](https://github.com/jtheoof/swappy/commit/9e7f3b2bbc5aa43723284d4ecfc75807f920cb74))
36 |
37 |
38 | ### Bug Fixes
39 |
40 | * **config:** fix segfault at exit ([182322d](https://github.com/jtheoof/swappy/commit/182322dd5bbdc6779052a949f7813c5c348123e3))
41 | * **ui:** add non-versioned fontawesome ([3941fec](https://github.com/jtheoof/swappy/commit/3941feccf0720836c23566c7fcf9a96b27a62a37))
42 |
43 | ## [1.6.0](https://github.com/jtheoof/swappy/compare/v1.5.1...v1.6.0) (2025-08-18)
44 |
45 |
46 | ### Features
47 |
48 | * auto saving on quit app ([72a511c](https://github.com/jtheoof/swappy/commit/72a511c435e9e605b8269669df02996c6b2b5e10))
49 | * **config:** custom_color config option ([726159d](https://github.com/jtheoof/swappy/commit/726159d81b073f13c6e7661392c0440d4af162bf))
50 | * **i18n:** add zh_CN locale ([9e7f3b2](https://github.com/jtheoof/swappy/commit/9e7f3b2bbc5aa43723284d4ecfc75807f920cb74))
51 |
52 |
53 | ### Bug Fixes
54 |
55 | * **config:** fix segfault at exit ([182322d](https://github.com/jtheoof/swappy/commit/182322dd5bbdc6779052a949f7813c5c348123e3))
56 | * **ui:** add non-versioned fontawesome ([3941fec](https://github.com/jtheoof/swappy/commit/3941feccf0720836c23566c7fcf9a96b27a62a37))
57 |
58 | ### [1.5.1](https://github.com/jtheoof/swappy/compare/v1.5.0...v1.5.1) (2022-11-20)
59 |
60 |
61 | ### Bug Fixes
62 |
63 | * **ui:** use *-symbolic variant of toolbar icons ([5dc44f8](https://github.com/jtheoof/swappy/commit/5dc44f8970b0f6cdf21466bc2689ec2aa93a4385)), closes [#34](https://github.com/jtheoof/swappy/issues/34)
64 |
65 | ## [1.5.0](https://github.com/jtheoof/swappy/compare/v1.4.0...v1.5.0) (2022-11-18)
66 |
67 |
68 | ### Features
69 |
70 | * **config:** add early_exit option ([60da549](https://github.com/jtheoof/swappy/commit/60da5491e243c9edd85f6225326a68ae5e3edfd5))
71 | * **config:** allow paint_mode to be configured through config file ([2f35f02](https://github.com/jtheoof/swappy/commit/2f35f02b4e89bf67b6e9cc461e874331d8ce2a4c))
72 | * **config:** try to create `save_dir` if it does not exist ([4fb291a](https://github.com/jtheoof/swappy/commit/4fb291ad4b0b116afeaa7094b040083111b74674))
73 | * **ui:** allow filling rectangles and ellipsis ([8ee55f7](https://github.com/jtheoof/swappy/commit/8ee55f7d52ce6ac71752981863f5795fef460049)), closes [#120](https://github.com/jtheoof/swappy/issues/120)
74 |
75 | ## [1.4.0](https://github.com/jtheoof/swappy/compare/v1.3.1...v1.4.0) (2021-09-06)
76 |
77 |
78 | ### Features
79 |
80 | * **draw:** draw shape from center if holding control ([d80c361](https://github.com/jtheoof/swappy/commit/d80c3614895d3b5da479831c651cc1afa2fcf916))
81 | * **i18n:** add french translations ([cacb283](https://github.com/jtheoof/swappy/commit/cacb2830e4cc41010d6ab96655054d2eb1651651))
82 |
83 |
84 | ### Bug Fixes
85 |
86 | * **desktop:** remove annotation from desktop categories ([0d383f6](https://github.com/jtheoof/swappy/commit/0d383f690b99026c340eab1efa590c48d54e7368))
87 | * **desktop:** various fixes ([42425c0](https://github.com/jtheoof/swappy/commit/42425c0657a65b3f66ba4f64b1727c8198a70684))
88 | * **i18n:** add german translations to desktop file ([c6b09e5](https://github.com/jtheoof/swappy/commit/c6b09e56399369b14a8de090a2239350dbe4aca8))
89 | * **i18n:** add turkish translation to desktop file ([fa5769e](https://github.com/jtheoof/swappy/commit/fa5769e9406b8ab1b67aca3bff2656850362491e))
90 | * **i18n:** properly set translation domain during layout init ([5301aeb](https://github.com/jtheoof/swappy/commit/5301aebd5e5534453621db7168b8afac5d7810f2)), closes [#92](https://github.com/jtheoof/swappy/issues/92)
91 | * **pixbuf:** handle invalid input file ([cdbd06d](https://github.com/jtheoof/swappy/commit/cdbd06d7af94b4aedfc2bda2231da8853f775f3a))
92 | * **pixbuf:** handle overflow when filename_format is too long ([185575b](https://github.com/jtheoof/swappy/commit/185575bf75281eba8a0bc49b3da59225bdd9e1c7)), closes [#74](https://github.com/jtheoof/swappy/issues/74)
93 | * **po:** update GETTEXT_PACKAGE value with project name ([7fd552e](https://github.com/jtheoof/swappy/commit/7fd552e8c41f29711212d7f70edf61ac6ada7a7d))
94 | * **release:** properly check sha256 remote content ([91985c7](https://github.com/jtheoof/swappy/commit/91985c7994764f52c8e9d864db8ec9cf2eb1df5c)), closes [#90](https://github.com/jtheoof/swappy/issues/90)
95 |
96 | ### [1.3.1](https://github.com/jtheoof/swappy/compare/v1.3.0...v1.3.1) (2021-02-20)
97 |
98 | ## [1.3.0](https://github.com/jtheoof/swappy/compare/v1.2.1...v1.3.0) (2021-02-18)
99 |
100 |
101 | ### Features
102 |
103 | * **cli:** add configure options for filename save ([597f005](https://github.com/jtheoof/swappy/commit/597f0055b9c6230b25a7f7a7bf3f4e14c06b1fbb))
104 | * **i18n:** add brazilian portuguese translations ([4a0eb82](https://github.com/jtheoof/swappy/commit/4a0eb82369a0859fafdcce9d242c086cd2360a84))
105 | * **i18n:** add german translations ([b4be847](https://github.com/jtheoof/swappy/commit/b4be8476350771454b29b9ce29c62a3337acc736))
106 | * **i18n:** add turkish translations ([c8419da](https://github.com/jtheoof/swappy/commit/c8419da7faef14223ada6853942a6d11e2acf92f))
107 |
108 |
109 | ### Bug Fixes
110 |
111 | * **application:** unlink temp file coming from stdin ([c24e56a](https://github.com/jtheoof/swappy/commit/c24e56a165394e60b37534287e168e5d8e69627c)), closes [#80](https://github.com/jtheoof/swappy/issues/80)
112 | * **blur:** optimize blur to only render after commit ([27fcece](https://github.com/jtheoof/swappy/commit/27fcecedaeea49aaec6acdecbc51cbd865a13363))
113 | * **blur:** rgb24 is properly handled ([c04ed63](https://github.com/jtheoof/swappy/commit/c04ed63d26e5012215198f7b41a7f2232dac1ebe))
114 | * **clipboard:** wl-copy mimetype should be png ([a931acb](https://github.com/jtheoof/swappy/commit/a931acb2cff615badc63294ed121aba008f32ef8)), closes [#68](https://github.com/jtheoof/swappy/issues/68)
115 | * **notification:** notification shows the image icon ([eb53e5c](https://github.com/jtheoof/swappy/commit/eb53e5c2b28717f509dd58eab6da85897c0d6d9d))
116 | * **ui:** adjust rendering surface with proper scaling ([9b72571](https://github.com/jtheoof/swappy/commit/9b72571596f9313d4efd94a4b17da8b3733fd2de)), closes [#54](https://github.com/jtheoof/swappy/issues/54)
117 | * **ui:** commit state before copying or saving ([46e5854](https://github.com/jtheoof/swappy/commit/46e5854b3cce93a82984b19ca90e3f3337952fe2)), closes [#52](https://github.com/jtheoof/swappy/issues/52)
118 | * **ui:** compute window sizes and buffers properly ([5bcffdb](https://github.com/jtheoof/swappy/commit/5bcffdbb01cc6e56f9c0f37de899b46efe68ed4a)), closes [#56](https://github.com/jtheoof/swappy/issues/56)
119 |
120 | ### [1.2.1](https://github.com/jtheoof/swappy/compare/v1.2.0...v1.2.1) (2020-07-11)
121 |
122 |
123 | ### Bug Fixes
124 |
125 | * **text:** properly handle utf-8 chars ([717ab0c](https://github.com/jtheoof/swappy/commit/717ab0c2d1757e10bb4eef17d35ccd6a991705c4)), closes [#43](https://github.com/jtheoof/swappy/issues/43)
126 |
127 | ## [1.2.0](https://github.com/jtheoof/swappy/compare/v1.1.0...v1.2.0) (2020-07-05)
128 |
129 |
130 | ### Features
131 |
132 | * **i18n:** add translatable desktop file ([cf3d7a5](https://github.com/jtheoof/swappy/commit/cf3d7a5283a7b8c34b05996f87b608513e0830ca)), closes [#35](https://github.com/jtheoof/swappy/issues/35)
133 | * **i18n:** setup i18n for swappy ([5b3c8ad](https://github.com/jtheoof/swappy/commit/5b3c8aded8fd4f9d00aa660a24127de0e1791d7f))
134 |
135 | ## [1.2.0](https://github.com/jtheoof/swappy/compare/v1.1.0...v1.2.0) (2020-07-05)
136 |
137 |
138 | ### Features
139 |
140 | * **i18n:** add translatable desktop file ([cf3d7a5](https://github.com/jtheoof/swappy/commit/cf3d7a5283a7b8c34b05996f87b608513e0830ca)), closes [#35](https://github.com/jtheoof/swappy/issues/35)
141 | * **i18n:** setup i18n for swappy ([5b3c8ad](https://github.com/jtheoof/swappy/commit/5b3c8aded8fd4f9d00aa660a24127de0e1791d7f))
142 |
143 | ## [1.1.0](https://github.com/jtheoof/swappy/compare/v1.0.1...v1.1.0) (2020-06-23)
144 |
145 |
146 | ### Features
147 |
148 | * **cli:** add -v and --version flags ([e32c024](https://github.com/jtheoof/swappy/commit/e32c02454ae4ec6ac30549d5fa9e80c2b64edb72))
149 |
150 | ### [1.0.1](https://github.com/jtheoof/swappy/compare/v1.0.0...v1.0.1) (2020-06-21)
151 |
152 |
153 | ### Bug Fixes
154 |
155 | * **cli:** stop showing -g option ([ee06d66](https://github.com/jtheoof/swappy/commit/ee06d6685f6f59ffce544b45d7b51f3f4523348b))
156 |
157 | ## 1.0.0 (2020-06-21)
158 |
159 |
160 | ### ⚠ BREAKING CHANGES
161 |
162 | * We do no support the `-g` option anymore.
163 |
164 | This tool simply makes more sense as the output of `grim` rather than
165 | trying to be `grim`.
166 |
167 | RIP my ugly wayland code, long live maintainable code.
168 |
169 | Next stop, rust?
170 |
171 | ### Features
172 |
173 | * **ui:** life is full of colors and joy ([a8c8be3](https://github.com/jtheoof/swappy/commit/a8c8be37ca996f3e1b752bca67eee594706bc08f))
174 | * init project ([efc3ecc](https://github.com/jtheoof/swappy/commit/efc3eccc9e21892a6b0979126a23d21d3d6a3b3d))
175 | * **application:** print final surface to file or stdout ([196f7f4](https://github.com/jtheoof/swappy/commit/196f7f4dea3ab569f0523171ae7c424b8e8423ee)), closes [#2](https://github.com/jtheoof/swappy/issues/2)
176 | * **application:** update app ([ce27741](https://github.com/jtheoof/swappy/commit/ce27741017554d6606e23434273f55476bc8ae37))
177 | * **blur:** add multiple passes logic ([f9737d7](https://github.com/jtheoof/swappy/commit/f9737d78c96a5d9f4566c94702c3ec4a41d9e219))
178 | * **blur:** remove blur configuration ([361be6a](https://github.com/jtheoof/swappy/commit/361be6aa8085143d9fd721e4c315c6b9e6fbdfca))
179 | * **blur:** use rect blur instead of brush ([1be7798](https://github.com/jtheoof/swappy/commit/1be7798a8bcfc494b20489e2e1f8b0245f4b5e84)), closes [#17](https://github.com/jtheoof/swappy/issues/17)
180 | * **buffer:** ability to read from stdin ([02bc464](https://github.com/jtheoof/swappy/commit/02bc46456453e8530a3c9f1289dfce7e71371945))
181 | * **buffer:** add file image support ([f6c189c](https://github.com/jtheoof/swappy/commit/f6c189c7b7f35ca4da75abaac0bd85c3d5ce5b09))
182 | * **clipboard:** use wl-copy if present ([51b27d7](https://github.com/jtheoof/swappy/commit/51b27d768eef7fbbdab365fa94a81af5395b0e3e))
183 | * **config:** add show_panel config ([307f579](https://github.com/jtheoof/swappy/commit/307f57956f105d22de2d8242313517b6a79ed4e2)), closes [#12](https://github.com/jtheoof/swappy/issues/12)
184 | * **config:** have overridable defaults ([ef24851](https://github.com/jtheoof/swappy/commit/ef24851deec2d6b7f76ed0fbbcd31b54b336cae3)), closes [#1](https://github.com/jtheoof/swappy/issues/1)
185 | * **draw:** convert wl_shm_format to cairo_format ([c623939](https://github.com/jtheoof/swappy/commit/c623939e02238f053312ad6367e761aec254c6fe))
186 | * **draw:** draw the screencopy buffer ([2344414](https://github.com/jtheoof/swappy/commit/2344414102789975e6ce425a95e8b96159cf51ba))
187 | * **layer:** use geometry size ([290d3ca](https://github.com/jtheoof/swappy/commit/290d3ca230d32ec2ef4036bf9e32f1e711fecd84))
188 | * **paint:** introduce text paint ([3347bf2](https://github.com/jtheoof/swappy/commit/3347bf23bf17d4c2cc8e5b9bbadd657efafb28e7))
189 | * **screencopy:** add buffer creation through screencopy ([bff8687](https://github.com/jtheoof/swappy/commit/bff8687fc81ebb57a179b1f50300f9c0cda793e3))
190 | * **screencopy:** introduce screencopy features ([53c9770](https://github.com/jtheoof/swappy/commit/53c977080829c7e816db1a9ec45eb432f6b7b354))
191 | * **swappy:** copy to clipboard with CTRL+C ([b90500e](https://github.com/jtheoof/swappy/commit/b90500ed34defcb8ebc67965c4dbb5d068ee8049))
192 | * **swappy:** introduce file option ([c56df33](https://github.com/jtheoof/swappy/commit/c56df33d1880d22372e21ef0ebf5dd8805d65a76))
193 | * **swappy:** save to file with CTRL+S ([af0b1a1](https://github.com/jtheoof/swappy/commit/af0b1a11a21faac04f8b43c4c9ef616ab5fd2b78))
194 | * **text:** add controls in toggle panel ([c03f628](https://github.com/jtheoof/swappy/commit/c03f628de793e170d9f62c5b786fe18891bb6fa3))
195 | * **tool:** introduce blurring capability ([fae0aea](https://github.com/jtheoof/swappy/commit/fae0aeacab6fb28e17975097c8b4c5c7e5ad57fd)), closes [#17](https://github.com/jtheoof/swappy/issues/17)
196 | * **ui:** add binding for clear action ([2bdab68](https://github.com/jtheoof/swappy/commit/2bdab684e1eace53ad7b78414ad467d312dc10ad))
197 | * **ui:** add binding to toggle panel ([e8d2f12](https://github.com/jtheoof/swappy/commit/e8d2f12ce1737fa19972e5c4109e1c85cc2b157e))
198 | * **ui:** add keybindings for color change ([c5ec285](https://github.com/jtheoof/swappy/commit/c5ec285ee73ddf90df2cb571e1d6c61159605c8e))
199 | * **ui:** add keybindings for stroke size ([562a9a6](https://github.com/jtheoof/swappy/commit/562a9a6e92201677f31de126b646c619caf33863))
200 | * **ui:** add shortcuts for undo/redo ([d7e7f2b](https://github.com/jtheoof/swappy/commit/d7e7f2b5ffd46aa36bed6ecc6709aeb94cce64ae))
201 | * **ui:** add toggle panel button ([7674d7d](https://github.com/jtheoof/swappy/commit/7674d7db8ba8d97302a045af8d2383de37acb2d1)), closes [#24](https://github.com/jtheoof/swappy/issues/24)
202 | * **ui:** add undo/redo ([bcc1314](https://github.com/jtheoof/swappy/commit/bcc13140ebfdefa30431b288f089d23bb1df743e))
203 | * **ui:** life is full of colors and joy ([606cd34](https://github.com/jtheoof/swappy/commit/606cd3459de3908e5fecdb7a49162ef3a9b52ab7))
204 | * **ui:** replace popover by on screen elements ([8cd3f13](https://github.com/jtheoof/swappy/commit/8cd3f134bbd8e05523303914f6c8f3989e6b4502))
205 | * **wayland:** added xdg_output_manager ([7b3549f](https://github.com/jtheoof/swappy/commit/7b3549fdd86fe1a945e1988bf22042c0f8dd6ed0))
206 | * **wayland:** listing outputs ([5a55c8b](https://github.com/jtheoof/swappy/commit/5a55c8bbbd08ad717ddabac51be31483950d827f))
207 |
208 |
209 | ### Bug Fixes
210 |
211 | * **application:** fix file loop and use of GTK object after lifecycle ([320dae0](https://github.com/jtheoof/swappy/commit/320dae02d0c6dca3fa2fd7ca934a85483ac2dd35))
212 | * **application:** memory leak for pixbuf ([f9d70fc](https://github.com/jtheoof/swappy/commit/f9d70fc0e22274e6cbe74bfdf714cdf04e34053d))
213 | * **application:** properly save output file upon clean exit ([b5cc433](https://github.com/jtheoof/swappy/commit/b5cc433d75d77759cef139e0e232bde79196f886)), closes [#8](https://github.com/jtheoof/swappy/issues/8)
214 | * **application:** suffix saved file with png ([7f2f6da](https://github.com/jtheoof/swappy/commit/7f2f6da754571771475558233f5a47813ec278dd))
215 | * **blur:** adjust blur bounding box based on scaled monitor ([6b2ec90](https://github.com/jtheoof/swappy/commit/6b2ec90efd99e1979310b673ad40b3724669dac1))
216 | * **blur:** blur based on device scaling factor ([1699474](https://github.com/jtheoof/swappy/commit/1699474c39fc305492c8bb03063c4582af4dbf9e))
217 | * **blur:** use better glyph icon ([97cd607](https://github.com/jtheoof/swappy/commit/97cd6072c986c9a7c69306744390a6ddb6a44646))
218 | * **blur:** use rendered surface after commit ([46fb08d](https://github.com/jtheoof/swappy/commit/46fb08dce17a820fcb500d2b6ff02f7d682f3c18)), closes [#20](https://github.com/jtheoof/swappy/issues/20) [#22](https://github.com/jtheoof/swappy/issues/22)
219 | * **buffer:** properly include required functions ([d787586](https://github.com/jtheoof/swappy/commit/d787586b9ed1d7e855ae2d416914d619636f41b1)), closes [#10](https://github.com/jtheoof/swappy/issues/10)
220 | * **clipboard:** handle bad write to pipe fd ([f963a76](https://github.com/jtheoof/swappy/commit/f963a76c5c01b9b5f81b97118bf1b9e6990d995d))
221 | * **clipboard:** memory leak for pixbuf ([665295b](https://github.com/jtheoof/swappy/commit/665295b497d7ef124d5a2eeb7eb76964fdb3566a))
222 | * **dependencies:** include glib2 ([992d97e](https://github.com/jtheoof/swappy/commit/992d97e94d2ebd32ac3e1901910050fae1954ed0)), closes [#11](https://github.com/jtheoof/swappy/issues/11)
223 | * **file:** properly check file system errors if any ([541ec21](https://github.com/jtheoof/swappy/commit/541ec21ca0efdec4d06c96f5ad1768b4219ed4ab))
224 | * **init:** fix segfault for unknown flags ([f4e9a19](https://github.com/jtheoof/swappy/commit/f4e9a19407d8d1bfa59c08f6bf97617c662e1ac0))
225 | * **init:** properly handle null geometry ([c4ea305](https://github.com/jtheoof/swappy/commit/c4ea305ae6ac9429bf44fdfc7218a30363439582))
226 | * **man:** remove blur_level related config ([ceb907a](https://github.com/jtheoof/swappy/commit/ceb907a5dc736c7d44318b35fb911aeb2360d851))
227 | * **meson:** able to build on standard platforms ([8abc5d5](https://github.com/jtheoof/swappy/commit/8abc5d52ec2962a111c6d44cdb5e9e209ac219c7))
228 | * **meson:** remove useless cname in meson res file ([9b8ea64](https://github.com/jtheoof/swappy/commit/9b8ea64307b33eb010b8ba043919f3eddf935b19))
229 | * **paint:** fix memory leak for brush paints ([aed2bfe](https://github.com/jtheoof/swappy/commit/aed2bfe29465aa5161155c1edda9d03cac607906))
230 | * **pixbuf:** possibly fix core dump ([8a82e79](https://github.com/jtheoof/swappy/commit/8a82e796bb871b57fa6ab4d2ed8d761033370d8c))
231 | * **pixbuf:** properly grab pixbuf size from cairo surface ([2adcf94](https://github.com/jtheoof/swappy/commit/2adcf944f4a7f2da5b5edf49a37922c43b2e477e)), closes [#6](https://github.com/jtheoof/swappy/issues/6)
232 | * **render:** better handler empty buffer ([acf2379](https://github.com/jtheoof/swappy/commit/acf2379ba3117ba6eb8c426e85a60ce71a3abe67))
233 | * **render:** draw from last to first ([4b69ada](https://github.com/jtheoof/swappy/commit/4b69ada9a1469d3b6e106e07bf7155836b31d613))
234 | * **render:** fix arrow glitch with 0 ftx ([ec6e6ab](https://github.com/jtheoof/swappy/commit/ec6e6abae7629800fec4c715957c4932946f51ed))
235 | * **render:** properly scale arrow along with stroke size ([75bfc10](https://github.com/jtheoof/swappy/commit/75bfc10fb7a5507b66bd6d19ab06f2f6a393bb6a))
236 | * **resources:** compile resources and fix error management ([05d87c9](https://github.com/jtheoof/swappy/commit/05d87c929ff8b3311cd5db111cd2f53a32c35a19))
237 | * **string:** fix algo to insert chars at location ([bc3264e](https://github.com/jtheoof/swappy/commit/bc3264e9f11bb4f3a02d7f5ae92ef8a4d2b42513))
238 | * **ui:** add stroke size increase/decrease/reset ([5930c99](https://github.com/jtheoof/swappy/commit/5930c99b9e0208148d6bc8cf0fc3aa8f69dbd36d))
239 | * **ui:** move paint area inside GtkFixed ([50e7c97](https://github.com/jtheoof/swappy/commit/50e7c97042805f5550d2a62d45c8e49208d7632d))
240 | * **ui:** prevent focus in panel buttons ([903ad11](https://github.com/jtheoof/swappy/commit/903ad114f516981c8d0644f704af9c722f74a61f))
241 | * **ui:** small tweaks ([2b73777](https://github.com/jtheoof/swappy/commit/2b73777142141598c14d37d1b6fa9573de12d914))
242 | * **ui:** tweak button sizes ([425f455](https://github.com/jtheoof/swappy/commit/425f455ab7665a046060fe140c861aeb7ea8209b))
243 | * **ui/render:** adjust rendering based on window size ([445980b](https://github.com/jtheoof/swappy/commit/445980bbf4702e59113fab506b2e9e36ad931666)), closes [#6](https://github.com/jtheoof/swappy/issues/6)
244 | * **wayland:** initialize done copies to 0 ([65cefc1](https://github.com/jtheoof/swappy/commit/65cefc1da7fed86508301250ffc1b6dbc9fd3692))
245 | * **wayland:** replace g_error by g_warning ([64bfc2b](https://github.com/jtheoof/swappy/commit/64bfc2b3a71ed00d0dc1102501ac85792735833f))
246 | * **window:** quit when delete event is received ([0c5e458](https://github.com/jtheoof/swappy/commit/0c5e458d4c44a2e2e2b4451b4576724aef2a06b0))
247 |
248 |
249 | * refactor!(wayland): remove wayland code ([204a93e](https://github.com/jtheoof/swappy/commit/204a93eb0f696bc7be8335d46212c6024e3b2c51))
250 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Jeremy Attali
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # swappy
2 |
3 | A Wayland native snapshot and editor tool, inspired by [Snappy] on macOS. Works great with [grim], [slurp] and [sway]. But can easily work with other screen copy tools that can output a final image to `stdout`. See [below](#example-usage).
4 |
5 | ## Screenshot
6 |
7 | 
8 |
9 | ## Example usage
10 |
11 | Output of `grim` (or any tool outputting an image file):
12 |
13 | ```sh
14 | grim -g "$(slurp)" - | swappy -f -
15 | ```
16 |
17 | Swappshot a PNG file:
18 |
19 | ```sh
20 | swappy -f "~/Desktop/my-gnome-saved-file.png"
21 | ```
22 |
23 | Print final surface to stdout (useful to pipe with other tools):
24 |
25 | ```sh
26 | grim -g "$(slurp)" - | swappy -f - -o - | pngquant -
27 | ```
28 |
29 | Grab a swappshot from a specific window under Sway, using `swaymsg` and `jq`:
30 |
31 | ```sh
32 | grim -g "$(swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp)" - | swappy -f -
33 | ```
34 |
35 | ## Config
36 |
37 | The config file is located at `$XDG_CONFIG_HOME/swappy/config` or at `$HOME/.config/swappy/config`.
38 |
39 | The file follows the GLib `conf` format. See the `man` page for details. There is example config file [here](example/config).
40 |
41 | The following lines can be used as swappy's default:
42 |
43 | ```
44 | [Default]
45 | save_dir=$HOME/Desktop
46 | save_filename_format=swappy-%Y%m%d-%H%M%S.png
47 | show_panel=false
48 | line_size=5
49 | text_size=20
50 | text_font=sans-serif
51 | paint_mode=brush
52 | early_exit=false
53 | fill_shape=false
54 | auto_save=false
55 | custom_color=rgba(193,125,17,1)
56 | transparent=false
57 | transparency=50
58 | ```
59 |
60 | - `save_dir` is where swappshots will be saved, can contain env variables, when it does not exist, swappy attempts to create it first, but does not abort if directory creation fails
61 | - `save_filename_format`: is the filename template, if it contains a date format, this will be parsed into a timestamp. Format is detailed in [strftime(3)](https://man.archlinux.org/man/strftime.3). If this date format is missing, filename will have no timestamp
62 | - `show_panel` is used to toggle the paint panel on or off upon startup
63 | - `line_size` is the default line size (must be between 1 and 50)
64 | - `text_size` is the default text size (must be between 10 and 50)
65 | - `text_font` is the font used to render text, its format is pango friendly
66 | - `paint_mode` is the mode activated at application start (must be one of: brush|text|rectangle|ellipse|arrow|blur, matching is case-insensitive)
67 | - `early_exit` is used to make the application exit after saving the picture or copying it to the clipboard
68 | - `fill_shape` is used to toggle shape filling (for the rectangle and ellipsis tools) on or off upon startup
69 | - `auto_save` is used to toggle auto saving of final buffer to `save_dir` upon exit
70 | - `custom_color` is used to set a default value for the custom color
71 | - `transparency` is used to set transparency of everything that is drawn during startup
72 | - `transparent` is used to toggle transparency during startup
73 |
74 |
75 | ## Keyboard Shortcuts
76 |
77 | - `Ctrl+b`: Toggle Paint Panel
78 |
79 |
80 |
81 | - `b`: Switch to Brush
82 | - `e` `t`: Switch to Text (Editor)
83 | - `r` `s`: Switch to Rectangle (Square)
84 | - `c` `o`: Switch to Ellipse (Circle)
85 | - `a`: Switch to Arrow
86 | - `d`: Switch to Blur (`d` stands for droplet)
87 |
88 |
89 |
90 | - `R`: Use Red Color
91 | - `G`: Use Green Color
92 | - `B`: Use Blue Color
93 | - `C`: Use Custom Color
94 | - `Minus`: Reduce Stroke Size
95 | - `Plus`: Increase Stroke Size
96 | - `Equal`: Reset Stroke Size
97 | - `f`: Toggle Shape Filling
98 | - `x` `k`: Clear Paints (cannot be undone)
99 | - `T`: Toggle Transparency
100 |
101 |
102 |
103 | - `Ctrl`: Center Shape (Rectangle & Ellipse) based on draw start
104 |
105 |
106 |
107 | - `Ctrl+z`: Undo
108 | - `Ctrl+Shift+z` or `Ctrl+y`: Redo
109 | - `Ctrl+s`: Save to file (see man page)
110 | - `Ctrl+c`: Copy to clipboard
111 | - `Escape` or `q` or `Ctrl+w`: Quit swappy
112 |
113 | ## Limitations
114 |
115 | - **Copy**: If you don't have [wl-clipboard] installed, copy to clipboard won't work if you close swappy (the content of the clipboard is lost). This because GTK 3.24 [has not implemented persistent storage on wayland backend yet](https://gitlab.gnome.org/GNOME/gtk/blob/3.24.13/gdk/wayland/gdkdisplay-wayland.c#L857). We need to do it on the [Wayland level](https://github.com/swaywm/wlr-protocols/blob/master/unstable/wlr-data-control-unstable-v1.xml), or wait for GTK 4. For now, we use `wl-copy` if installed and revert to `gtk` clipboard if not found.
116 | - **Fonts**: Swappy relies on Font Awesome 5 being present to properly render the icons. On Arch you can simply install those with: `sudo pacman -S otf-font-awesome`
117 | - **Output Format**: Only PNG is supported.
118 |
119 | ## Installation
120 |
121 | - [Arch Linux](https://archlinux.org/packages/extra/x86_64/swappy/)
122 | - [Arch Linux (git)](https://aur.archlinux.org/packages/swappy-git)
123 | - [Fedora](https://src.fedoraproject.org/rpms/swappy)
124 | - [Gentoo](https://packages.gentoo.org/packages/gui-apps/swappy)
125 | - [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/swappy)
126 | - [Void Linux](https://github.com/void-linux/void-packages/tree/master/srcpkgs/swappy)
127 |
128 | ## Building from source
129 |
130 | Install dependencies (on Arch, name can vary for other distros):
131 |
132 | - meson
133 | - ninja
134 | - cairo
135 | - pango
136 | - gtk
137 | - glib2
138 | - scdoc
139 |
140 | Optional dependencies:
141 |
142 | - `wl-clipboard` (to make sure the copy is saved if you close swappy)
143 | - `otf-font-awesome` (to draw the paint icons properly)
144 |
145 | Then run:
146 |
147 | ```sh
148 | meson setup build
149 | ninja -C build
150 | ```
151 |
152 | ### i18n
153 |
154 | This section is for developers, maintainers and translators.
155 |
156 | To add support to a new locale or when translations are updated:
157 |
158 | 1. Update `src/po/LINGUAS` (when new locales are added)
159 | 2. Generate a new `po` file (ignore and do not commit potential noise in other files):
160 |
161 | ```sh
162 | ninja -C build swappy-update-po
163 | ```
164 |
165 | To rebuild the base template (should happen less often):
166 |
167 | ```sh
168 | ninja -C build swappy-pot
169 | ```
170 |
171 | See the [meson documentation](https://mesonbuild.com/Localisation.html) for details.
172 |
173 | ## Contributing
174 |
175 | Pull requests are welcome. This project uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to automate changelog generation.
176 |
177 | ## Release
178 |
179 | We rely on [standard-version](https://github.com/conventional-changelog/standard-version) which is part of the JavaScript ecosystem but works well with any project.
180 |
181 | ```sh
182 | ./script/github-release
183 | ```
184 |
185 | Make sure everything is valid in the Draft release, then publish the draft.
186 |
187 | Release tarballs are signed with this PGP key: `F44D05A50F6C9EB5C81BCF966A6B35DBE9442683`
188 |
189 | ## License
190 |
191 | MIT
192 |
193 | [snappy]: http://snappy-app.com/
194 | [slurp]: https://github.com/emersion/slurp
195 | [grim]: https://github.com/emersion/grim
196 | [sway]: https://github.com/swaywm/sway
197 | [wl-clipboard]: https://github.com/bugaevc/wl-clipboard
198 |
--------------------------------------------------------------------------------
/docs/images/screenshot-1.0.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/docs/images/screenshot-1.0.0.png
--------------------------------------------------------------------------------
/docs/images/screenshot-beta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/docs/images/screenshot-beta.png
--------------------------------------------------------------------------------
/example/config:
--------------------------------------------------------------------------------
1 | [Default]
2 | save_dir=$HOME/Desktop
3 | line_size=5
4 | text_size=20
5 | text_font=sans-serif
6 |
--------------------------------------------------------------------------------
/include/algebra.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | struct gaussian_kernel {
6 | gdouble *kernel;
7 | gint size;
8 | gdouble sigma;
9 | gdouble sum;
10 | };
11 |
12 | struct gaussian_kernel *gaussian_kernel(gint width, gdouble sigma);
13 | void gaussian_kernel_free(gpointer data);
14 |
--------------------------------------------------------------------------------
/include/application.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "swappy.h"
4 |
5 | bool application_init(struct swappy_state *state);
6 | int application_run(struct swappy_state *state);
7 | void application_finish(struct swappy_state *state);
8 |
9 | /* Glade signals */
10 | void window_keypress_handler(GtkWidget *widget, GdkEventKey *event,
11 | struct swappy_state *state);
12 | gboolean window_delete_handler(GtkWidget *widget, GdkEvent *event,
13 | struct swappy_state *state);
14 |
15 | void pane_toggled_handler(GtkWidget *widget, struct swappy_state *state);
16 | void undo_clicked_handler(GtkWidget *widget, struct swappy_state *state);
17 | void redo_clicked_handler(GtkWidget *widget, struct swappy_state *state);
18 |
19 | gboolean draw_area_handler(GtkWidget *widget, cairo_t *cr,
20 | struct swappy_state *state);
21 | gboolean draw_area_configure_handler(GtkWidget *widget,
22 | GdkEventConfigure *event,
23 | struct swappy_state *state);
24 | void draw_area_button_press_handler(GtkWidget *widget, GdkEventButton *event,
25 | struct swappy_state *state);
26 | void draw_area_button_release_handler(GtkWidget *widget, GdkEventButton *event,
27 | struct swappy_state *state);
28 | void draw_area_motion_notify_handler(GtkWidget *widget, GdkEventMotion *event,
29 | struct swappy_state *state);
30 |
31 | void brush_clicked_handler(GtkWidget *widget, struct swappy_state *state);
32 | void text_clicked_handler(GtkWidget *widget, struct swappy_state *state);
33 | void rectangle_clicked_handler(GtkWidget *widget, struct swappy_state *state);
34 | void ellipse_clicked_handler(GtkWidget *widget, struct swappy_state *state);
35 | void arrow_clicked_handler(GtkWidget *widget, struct swappy_state *state);
36 | void blur_clicked_handler(GtkWidget *widget, struct swappy_state *state);
37 |
38 | void copy_clicked_handler(GtkWidget *widget, struct swappy_state *state);
39 | void save_clicked_handler(GtkWidget *widget, struct swappy_state *state);
40 | void clear_clicked_handler(GtkWidget *widget, struct swappy_state *state);
41 |
42 | void color_red_clicked_handler(GtkWidget *widget, struct swappy_state *state);
43 | void color_green_clicked_handler(GtkWidget *widget, struct swappy_state *state);
44 | void color_blue_clicked_handler(GtkWidget *widget, struct swappy_state *state);
45 |
46 | void color_custom_clicked_handler(GtkWidget *widget,
47 | struct swappy_state *state);
48 | void color_custom_color_set_handler(GtkWidget *widget,
49 | struct swappy_state *state);
50 |
51 | void stroke_size_decrease_handler(GtkWidget *widget,
52 | struct swappy_state *state);
53 | void stroke_size_reset_handler(GtkWidget *widget, struct swappy_state *state);
54 | void stroke_size_increase_handler(GtkWidget *widget,
55 | struct swappy_state *state);
56 |
57 | void text_size_decrease_handler(GtkWidget *widget, struct swappy_state *state);
58 | void text_size_reset_handler(GtkWidget *widget, struct swappy_state *state);
59 | void text_size_increase_handler(GtkWidget *widget, struct swappy_state *state);
60 |
61 | void transparency_decrease_handler(GtkWidget *widget,
62 | struct swappy_state *state);
63 | void transparency_reset_handler(GtkWidget *widget, struct swappy_state *state);
64 | void transparency_increase_handler(GtkWidget *widget,
65 | struct swappy_state *state);
66 |
--------------------------------------------------------------------------------
/include/box.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "swappy.h"
4 |
5 | bool box_parse(struct swappy_box *box, const char *str);
6 | bool is_empty_box(struct swappy_box *box);
7 | bool intersect_box(struct swappy_box *a, struct swappy_box *b);
8 |
--------------------------------------------------------------------------------
/include/clipboard.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "swappy.h"
4 |
5 | bool clipboard_copy_drawing_area_to_selection(struct swappy_state *state);
6 |
--------------------------------------------------------------------------------
/include/config.h:
--------------------------------------------------------------------------------
1 | #include "swappy.h"
2 |
3 | #define CONFIG_LINE_SIZE_DEFAULT 5
4 | #define CONFIG_TEXT_FONT_DEFAULT "sans-serif"
5 | #define CONFIG_TEXT_SIZE_DEFAULT 20
6 | #define CONFIG_TRANSPARENCY_DEFAULT 50
7 | #define CONFIG_SHOW_PANEL_DEFAULT false
8 | #define CONFIG_SAVE_FILENAME_FORMAT_DEFAULT "swappy-%Y%m%d_%H%M%S.png"
9 | #define CONFIG_PAINT_MODE_DEFAULT SWAPPY_PAINT_MODE_BRUSH
10 | #define CONFIG_EARLY_EXIT_DEFAULT false
11 | #define CONFIG_FILL_SHAPE_DEFAULT false
12 | #define CONFIG_AUTO_SAVE_DEFAULT false
13 | #define CONFIG_CUSTOM_COLOR_DEFAULT "rgba(193,125,17,1)"
14 | #define CONFIG_TRANSPARENT_DEFAULT false
15 |
16 | void config_load(struct swappy_state *state);
17 | void config_free(struct swappy_state *state);
18 |
--------------------------------------------------------------------------------
/include/file.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | bool folder_exists(const char *path);
4 | bool file_exists(const char *path);
5 | char *file_dump_stdin_into_a_temp_file();
6 |
--------------------------------------------------------------------------------
/include/paint.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include "swappy.h"
6 |
7 | void paint_add_temporary(struct swappy_state *state, double x, double y,
8 | enum swappy_paint_type type);
9 | void paint_update_temporary_shape(struct swappy_state *state, double x,
10 | double y, gboolean is_control_pressed);
11 | void paint_update_temporary_text(struct swappy_state *state,
12 | GdkEventKey *event);
13 | void paint_update_temporary_str(struct swappy_state *state, char *event);
14 | void paint_update_temporary_text_clip(struct swappy_state *state, gdouble x,
15 | gdouble y);
16 | void paint_commit_temporary(struct swappy_state *state);
17 |
18 | void paint_free(gpointer data);
19 | void paint_free_all(struct swappy_state *state);
20 | void paint_free_list(GList **list);
21 |
--------------------------------------------------------------------------------
/include/pixbuf.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "swappy.h"
4 |
5 | GdkPixbuf *pixbuf_init_from_file(struct swappy_state *state);
6 | GdkPixbuf *pixbuf_get_from_state(struct swappy_state *state);
7 | void pixbuf_save_state_to_folder(GdkPixbuf *pixbuf, char *folder,
8 | char *filename_format);
9 | void pixbuf_save_to_file(GdkPixbuf *pixbuf, char *file);
10 | void pixbuf_save_to_stdout(GdkPixbuf *pixbuf);
11 | void pixbuf_scale_surface_from_widget(struct swappy_state *state,
12 | GtkWidget *widget);
13 | void pixbuf_free(struct swappy_state *state);
14 |
--------------------------------------------------------------------------------
/include/render.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "swappy.h"
4 |
5 | void render_state(struct swappy_state *state);
6 |
--------------------------------------------------------------------------------
/include/swappy.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #define MAX_PATH 4096
10 |
11 | #define SWAPPY_LINE_SIZE_MIN 1
12 | #define SWAPPY_LINE_SIZE_MAX 50
13 |
14 | #define SWAPPY_TEXT_SIZE_MIN 10
15 | #define SWAPPY_TEXT_SIZE_MAX 50
16 |
17 | #define SWAPPY_TRANSPARENCY_MIN 5
18 | #define SWAPPY_TRANSPARENCY_MAX 95
19 |
20 | enum swappy_paint_type {
21 | SWAPPY_PAINT_MODE_BRUSH = 0, /* Brush mode to draw arbitrary shapes */
22 | SWAPPY_PAINT_MODE_TEXT, /* Mode to draw texts */
23 | SWAPPY_PAINT_MODE_RECTANGLE, /* Rectangle shapes */
24 | SWAPPY_PAINT_MODE_ELLIPSE, /* Ellipse shapes */
25 | SWAPPY_PAINT_MODE_ARROW, /* Arrow shapes */
26 | SWAPPY_PAINT_MODE_BLUR, /* Blur mode */
27 | };
28 |
29 | enum swappy_paint_shape_operation {
30 | SWAPPY_PAINT_SHAPE_OPERATION_STROKE = 0, /* Used to stroke the shape */
31 | SWAPPY_PAINT_SHAPE_OPERATION_FILL, /* Used to fill the shape */
32 | };
33 |
34 | enum swappy_text_mode {
35 | SWAPPY_TEXT_MODE_EDIT = 0,
36 | SWAPPY_TEXT_MODE_DONE,
37 | };
38 |
39 | struct swappy_point {
40 | gdouble x;
41 | gdouble y;
42 | };
43 |
44 | struct swappy_paint_text {
45 | double r;
46 | double g;
47 | double b;
48 | double a;
49 | double s;
50 | gchar *font;
51 | gchar *text;
52 | glong cursor;
53 | struct swappy_point from;
54 | struct swappy_point to;
55 | enum swappy_text_mode mode;
56 | };
57 |
58 | struct swappy_paint_shape {
59 | double r;
60 | double g;
61 | double b;
62 | double a;
63 | double w;
64 | bool should_center_at_from;
65 | struct swappy_point from;
66 | struct swappy_point to;
67 | enum swappy_paint_type type;
68 | enum swappy_paint_shape_operation operation;
69 | };
70 |
71 | struct swappy_paint_brush {
72 | double r;
73 | double g;
74 | double b;
75 | double a;
76 | double w;
77 | GList *points;
78 | };
79 |
80 | struct swappy_paint_blur {
81 | struct swappy_point from;
82 | struct swappy_point to;
83 | cairo_surface_t *surface;
84 | };
85 |
86 | struct swappy_paint {
87 | enum swappy_paint_type type;
88 | bool can_draw;
89 | bool is_committed;
90 | union {
91 | struct swappy_paint_brush brush;
92 | struct swappy_paint_shape shape;
93 | struct swappy_paint_text text;
94 | struct swappy_paint_blur blur;
95 | } content;
96 | };
97 |
98 | struct swappy_box {
99 | int32_t x;
100 | int32_t y;
101 | int32_t width;
102 | int32_t height;
103 | };
104 |
105 | struct swappy_state_settings {
106 | double r;
107 | double g;
108 | double b;
109 | double a;
110 | double w;
111 | double t;
112 | int32_t tr;
113 | };
114 |
115 | struct swappy_state_ui {
116 | gboolean panel_toggled;
117 |
118 | GtkWindow *window;
119 | GtkIMContext *im_context;
120 |
121 | GtkWidget *area;
122 |
123 | GtkToggleButton *panel_toggle_button;
124 |
125 | // Undo / Redo
126 | GtkButton *undo;
127 | GtkButton *redo;
128 |
129 | // Painting Area
130 | GtkBox *painting_box;
131 | GtkRadioButton *brush;
132 | GtkRadioButton *text;
133 | GtkRadioButton *rectangle;
134 | GtkRadioButton *ellipse;
135 | GtkRadioButton *arrow;
136 | GtkRadioButton *blur;
137 |
138 | GtkRadioButton *red;
139 | GtkRadioButton *green;
140 | GtkRadioButton *blue;
141 | GtkRadioButton *custom;
142 | GtkColorButton *color;
143 |
144 | GtkButton *line_size;
145 | GtkButton *text_size;
146 | GtkButton *transparency;
147 | GtkButton *transparency_plus;
148 | GtkButton *transparency_minus;
149 |
150 | GtkToggleButton *fill_shape;
151 | GtkToggleButton *transparent;
152 | };
153 |
154 | struct swappy_config {
155 | char *config_file;
156 | char *save_dir;
157 | char *save_filename_format;
158 | gint8 paint_mode;
159 | gboolean fill_shape;
160 | gboolean transparent;
161 | gboolean show_panel;
162 | guint32 line_size;
163 | guint32 text_size;
164 | guint32 transparency;
165 | char *text_font;
166 | gboolean early_exit;
167 | gboolean auto_save;
168 | char *custom_color;
169 | };
170 |
171 | struct swappy_state {
172 | GtkApplication *app;
173 |
174 | struct swappy_state_ui *ui;
175 | struct swappy_config *config;
176 |
177 | GdkPixbuf *original_image;
178 | cairo_surface_t *original_image_surface;
179 | cairo_surface_t *rendering_surface;
180 |
181 | gdouble scaling_factor;
182 |
183 | enum swappy_paint_type mode;
184 |
185 | /* Options */
186 | char *file_str;
187 | char *output_file;
188 |
189 | char *temp_file_str;
190 |
191 | struct swappy_box *window;
192 | struct swappy_box *geometry;
193 |
194 | GList *paints;
195 | GList *redo_paints;
196 | struct swappy_paint *temp_paint;
197 |
198 | struct swappy_state_settings settings;
199 |
200 | int argc;
201 | char **argv;
202 | };
203 |
--------------------------------------------------------------------------------
/include/util.h:
--------------------------------------------------------------------------------
1 |
2 | #pragma once
3 |
4 | #include
5 |
6 | glong string_get_nb_bytes_until(gchar *str, glong until);
7 | gchar *string_remove_at(char *str, glong pos);
8 | gchar *string_insert_chars_at(gchar *str, gchar *chars, glong pos);
9 | void pixel_data_print(guint32 pixel);
10 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project(
2 | 'swappy',
3 | 'c',
4 | version: '1.8.0',
5 | license: 'MIT',
6 | meson_version: '>=0.48.0',
7 | default_options: [
8 | 'c_std=c11',
9 | 'warning_level=2',
10 | 'werror=true',
11 | ],
12 | )
13 |
14 | version = '"@0@"'.format(meson.project_version())
15 | git = find_program('git', native: true, required: false)
16 | if git.found()
17 | git_commit = run_command([git, 'rev-parse', '--short', 'HEAD'], check:true)
18 | git_branch = run_command([git, 'rev-parse', '--abbrev-ref', 'HEAD'], check:true)
19 | if git_commit.returncode() == 0 and git_branch.returncode() == 0
20 | version = '"@0@-@1@ (" __DATE__ ", branch \'@2@\')"'.format(
21 | meson.project_version(),
22 | git_commit.stdout().strip(),
23 | git_branch.stdout().strip(),
24 | )
25 | endif
26 | endif
27 | add_project_arguments('-DSWAPPY_VERSION=@0@'.format(version), language: 'c')
28 |
29 | add_project_arguments('-Wno-unused-parameter', language: 'c')
30 |
31 | swappy_inc = include_directories('include')
32 |
33 | cc = meson.get_compiler('c')
34 |
35 | if cc.get_id() == 'clang'
36 | message('clang')
37 | add_global_arguments('-Wno-missing-field-initializers', language: 'c')
38 | endif
39 |
40 | cairo = dependency('cairo')
41 | pango = dependency('pango')
42 | math = cc.find_library('m')
43 | gtk = dependency('gtk+-3.0', version: '>=3.20.0')
44 | gio = dependency('gio-2.0')
45 |
46 | subdir('res')
47 | subdir('src/po')
48 |
49 | executable(
50 | 'swappy',
51 | swappy_resources,
52 | files([
53 | 'src/main.c',
54 | 'src/algebra.c',
55 | 'src/application.c',
56 | 'src/box.c',
57 | 'src/config.c',
58 | 'src/clipboard.c',
59 | 'src/file.c',
60 | 'src/paint.c',
61 | 'src/pixbuf.c',
62 | 'src/render.c',
63 | 'src/util.c',
64 | ]),
65 | dependencies: [
66 | cairo,
67 | pango,
68 | gio,
69 | gtk,
70 | math,
71 | ],
72 | link_args: '-rdynamic',
73 | include_directories: [swappy_inc],
74 | install: true,
75 | )
76 |
77 | scdoc = find_program('scdoc', required: get_option('man-pages'))
78 |
79 | if scdoc.found()
80 | sh = find_program('sh')
81 |
82 | man_pages = ['swappy.1.scd']
83 |
84 | mandir = get_option('mandir')
85 |
86 | foreach src : man_pages
87 | topic = src.split('.')[0]
88 | section = src.split('.')[1]
89 | output = '@0@.@1@'.format(topic, section)
90 |
91 | custom_target(
92 | output,
93 | input: src,
94 | output: output,
95 | command: [
96 | sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.path(), output)
97 | ],
98 | install: true,
99 | install_dir: '@0@/man@1@'.format(mandir, section)
100 | )
101 | endforeach
102 | endif
103 |
--------------------------------------------------------------------------------
/meson_options.txt:
--------------------------------------------------------------------------------
1 | option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages')
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swappy",
3 | "version": "1.8.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/jtheoof/swappy.git"
7 | },
8 | "author": "Jeremy Attali"
9 | }
10 |
--------------------------------------------------------------------------------
/res/icons/hicolor/scalable/apps/swappy.svg:
--------------------------------------------------------------------------------
1 |
2 |
119 |
--------------------------------------------------------------------------------
/res/meson.build:
--------------------------------------------------------------------------------
1 | # Import the gnome module and use a GNOME function to ensure that application
2 | # resources will be compiled.
3 | gnome = import('gnome')
4 |
5 | # Icons
6 | install_subdir('icons',
7 | install_dir: join_paths(get_option('datadir')),
8 | )
9 |
10 | swappy_resources = gnome.compile_resources('swappy',
11 | 'swappy.gresource.xml'
12 | )
13 |
--------------------------------------------------------------------------------
/res/style/swappy.css:
--------------------------------------------------------------------------------
1 | .drawing .text-button {
2 | font-family: "FontAwesome 5 Free Solid", "FontAwesome";
3 | padding: 4px;
4 | }
5 |
6 | .drawing .text-button radio,
7 | .color-box .text-button radio {
8 | padding: 0;
9 | }
10 |
11 | .color-box button {
12 | padding: 6px 10px;
13 | }
14 |
15 | .color-box image {
16 | border-radius: 50px;
17 | }
18 |
19 | .color-box .color-red image {
20 | background-color: rgb(255, 0, 0);
21 | }
22 |
23 | .color-box .color-green image {
24 | background-color: rgb(0, 255, 0);
25 | }
26 |
27 | .color-box .color-blue image {
28 | background-color: rgb(0, 0, 255);
29 | }
30 |
--------------------------------------------------------------------------------
/res/swappy.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | style/swappy.css
5 | swappy.glade
6 |
7 |
8 |
--------------------------------------------------------------------------------
/script/bump-meson-build.js:
--------------------------------------------------------------------------------
1 | const projectVersionRegExp = /version: '(?\d+\.\d+\.\d+)',/;
2 |
3 | module.exports.readVersion = function (contents) {
4 | const matches = contents.match(projectVersionRegExp);
5 |
6 | return matches ? matches[1] : "unknown";
7 | };
8 |
9 | module.exports.writeVersion = function (_contents, version) {
10 | return _contents.replace(projectVersionRegExp, `version: '${version}',`);
11 | };
12 |
--------------------------------------------------------------------------------
/script/github-release:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | declare -r git_root=$(git rev-parse --show-toplevel)
6 | declare -r app_name="swappy"
7 | declare -r release_folder="$git_root/release"
8 |
9 | declare version=""
10 |
11 | die() {
12 | echo "$*" 1>&2
13 | exit 1
14 | }
15 |
16 | init() {
17 | command -v git >/dev/null 2>&1 || { echo >&2 "git required: pacman -S git"; exit 1; }
18 | command -v gh >/dev/null 2>&1 || { echo >&2 "github cli tool required to publish the release: pacman -S github-cli"; exit 1; }
19 | command -v npx >/dev/null 2>&1 || { echo >&2 "npx required for standard versioning the release: pacman -S npm"; exit 1; }
20 | command -v gpg >/dev/null 2>&1 || { echo >&2 "gpg required to sign the archive: pacman -S gnupg"; exit 1; }
21 |
22 | mkdir -p $release_folder
23 | }
24 |
25 | git_get_release_version() {
26 | version=$(git describe --tags --abbrev=0 | sed 's/^v//')
27 |
28 | if [ -z "$version" ]
29 | then
30 | die "version not found, is the git tag valid?"
31 | fi
32 |
33 | echo "found latest version: $version"
34 | }
35 |
36 | npx_standard_version() {
37 | echo "setting up new standard version with npx..."
38 | npx standard-version --sign
39 | }
40 |
41 | git_push_tags() {
42 | echo "pushing git tags..."
43 | git push --follow-tags
44 | }
45 |
46 |
47 | git_build_archive() {
48 | echo "building source archives..."
49 | cd $git_root
50 | git archive -o "$release_folder/$app_name-$version.tar.gz" --format tar.gz --prefix "$app_name-$version/" "v$version"
51 | }
52 |
53 | download_source_for_release() {
54 | echo "downloading source assets..."
55 | cd $release_folder
56 | curl --location --output github-$app_name-$version.tar.gz https://github.com/jtheoof/$app_name/archive/v$version.tar.gz
57 | }
58 |
59 | verify_sha256_checksums() {
60 | echo "verifying signatures..."
61 | cd $release_folder
62 | sha256sum $app_name-$version.tar.gz | awk '{ print $1 }' > $app_name-$version.tar.gz.sha256
63 |
64 | # sha256sum --check will exit if the checksums do not match
65 | echo "$(cat $app_name-$version.tar.gz.sha256) github-$app_name-$version.tar.gz" | sha256sum --check
66 | }
67 |
68 | gpg_sign_archive() {
69 | echo "signing source assets..."
70 | cd $release_folder
71 | gpg --output $app_name-$version.tar.gz.sig --detach-sign $app_name-$version.tar.gz
72 | }
73 |
74 | git_generate_changelog() {
75 | echo "generating changelog..."
76 | git diff "v$version"^ -- CHANGELOG.md | tail -n +9 | head -n -4 | sed 's/^+//g' > $release_folder/CHANGELOG-$version.md
77 | }
78 |
79 | github_create_release() {
80 | echo "creating github release..."
81 | gh release create --draft "v$version" \
82 | -F "$release_folder/CHANGELOG-$version.md" \
83 | "$release_folder/$app_name-$version.tar.gz" \
84 | "$release_folder/$app_name-$version.tar.gz.sig" \
85 | "$release_folder/CHANGELOG-$version.md"
86 | }
87 |
88 | main() {
89 | init
90 |
91 | npx_standard_version
92 | git_push_tags
93 | git_get_release_version
94 | git_build_archive
95 | # Turning off manual downloading from github
96 | # doing all the steps, including archive, ourselves.
97 | #download_source_for_release
98 | #verify_sha256_checksums
99 | git_generate_changelog
100 | gpg_sign_archive
101 | github_create_release
102 | }
103 |
104 | main "$@"
105 |
--------------------------------------------------------------------------------
/src/algebra.c:
--------------------------------------------------------------------------------
1 | #include "algebra.h"
2 |
3 | #include
4 | #include
5 |
6 | struct gaussian_kernel *gaussian_kernel(int width, double sigma) {
7 | double sum = 0;
8 | gint size = width * width + 1;
9 | double *kernel = g_new0(double, size);
10 | struct gaussian_kernel *gaussian = g_new(struct gaussian_kernel, 1);
11 | for (gint y = 0; y < width; y++) {
12 | for (gint x = 0; x < width; x++) {
13 | double j = y - width;
14 | double i = x - width;
15 | double cell = ((1.0 / (2.0 * G_PI * sigma)) *
16 | exp((-(i * i + j * j)) / (2.0 * sigma * sigma))) *
17 | 0xff;
18 | kernel[y * width + x] = cell;
19 | sum += cell;
20 | }
21 | }
22 |
23 | gaussian->kernel = kernel;
24 | gaussian->size = size;
25 | gaussian->sigma = sigma;
26 | gaussian->sum = sum;
27 |
28 | return gaussian;
29 | }
30 |
31 | void gaussian_kernel_free(gpointer data) {
32 | struct gaussian_kernel *gaussian = (struct gaussian_kernel *)data;
33 | if (gaussian != NULL) {
34 | g_free(gaussian->kernel);
35 | g_free(gaussian);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/application.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #include "clipboard.h"
10 | #include "config.h"
11 | #include "file.h"
12 | #include "paint.h"
13 | #include "pixbuf.h"
14 | #include "render.h"
15 | #include "swappy.h"
16 |
17 | static void update_ui_undo_redo(struct swappy_state *state) {
18 | GtkWidget *undo = GTK_WIDGET(state->ui->undo);
19 | GtkWidget *redo = GTK_WIDGET(state->ui->redo);
20 | gboolean undo_sensitive = g_list_length(state->paints) > 0;
21 | gboolean redo_sensitive = g_list_length(state->redo_paints) > 0;
22 | gtk_widget_set_sensitive(undo, undo_sensitive);
23 | gtk_widget_set_sensitive(redo, redo_sensitive);
24 | }
25 |
26 | static void update_ui_stroke_size_widget(struct swappy_state *state) {
27 | GtkButton *button = GTK_BUTTON(state->ui->line_size);
28 | char label[255];
29 | g_snprintf(label, 255, "%.0lf", state->settings.w);
30 | gtk_button_set_label(button, label);
31 | }
32 |
33 | static void update_ui_text_size_widget(struct swappy_state *state) {
34 | GtkButton *button = GTK_BUTTON(state->ui->text_size);
35 | char label[255];
36 | g_snprintf(label, 255, "%.0lf", state->settings.t);
37 | gtk_button_set_label(button, label);
38 | }
39 |
40 | static void update_ui_transparency_widget(struct swappy_state *state) {
41 | GtkButton *button = GTK_BUTTON(state->ui->transparency);
42 | char label[255];
43 | g_snprintf(label, 255, "%" PRId32, state->settings.tr);
44 | gtk_button_set_label(button, label);
45 | }
46 |
47 | static void update_ui_panel_toggle_button(struct swappy_state *state) {
48 | GtkWidget *painting_box = GTK_WIDGET(state->ui->painting_box);
49 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->panel_toggle_button);
50 | gboolean toggled = state->ui->panel_toggled;
51 |
52 | gtk_toggle_button_set_active(button, toggled);
53 | gtk_widget_set_visible(painting_box, toggled);
54 | }
55 |
56 | static void update_ui_fill_shape_toggle_button(struct swappy_state *state) {
57 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->fill_shape);
58 | gboolean toggled = state->config->fill_shape;
59 |
60 | gtk_toggle_button_set_active(button, toggled);
61 | }
62 |
63 | static void update_ui_transparent_toggle_button(struct swappy_state *state) {
64 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->transparent);
65 | gboolean toggled = state->config->transparent;
66 |
67 | gtk_toggle_button_set_active(button, toggled);
68 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->transparency), toggled);
69 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->transparency_minus), toggled);
70 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->transparency_plus), toggled);
71 | }
72 |
73 | void application_finish(struct swappy_state *state) {
74 | g_debug("application finishing, cleaning up");
75 | paint_free_all(state);
76 | pixbuf_free(state);
77 | cairo_surface_destroy(state->rendering_surface);
78 | cairo_surface_destroy(state->original_image_surface);
79 | if (state->temp_file_str) {
80 | g_info("deleting temporary file: %s", state->temp_file_str);
81 | if (g_unlink(state->temp_file_str) != 0) {
82 | g_warning("unable to delete temporary file: %s", state->temp_file_str);
83 | }
84 | g_free(state->temp_file_str);
85 | }
86 | g_free(state->file_str);
87 | g_free(state->geometry);
88 | g_free(state->window);
89 | g_object_unref(state->ui->im_context);
90 | g_free(state->ui);
91 |
92 | g_object_unref(state->app);
93 |
94 | config_free(state);
95 | }
96 |
97 | static void action_undo(struct swappy_state *state) {
98 | GList *first = state->paints;
99 |
100 | if (first) {
101 | state->paints = g_list_remove_link(state->paints, first);
102 | state->redo_paints = g_list_prepend(state->redo_paints, first->data);
103 |
104 | render_state(state);
105 | update_ui_undo_redo(state);
106 | }
107 | }
108 |
109 | static void action_redo(struct swappy_state *state) {
110 | GList *first = state->redo_paints;
111 |
112 | if (first) {
113 | state->redo_paints = g_list_remove_link(state->redo_paints, first);
114 | state->paints = g_list_prepend(state->paints, first->data);
115 |
116 | render_state(state);
117 | update_ui_undo_redo(state);
118 | }
119 | }
120 |
121 | static void action_clear(struct swappy_state *state) {
122 | paint_free_all(state);
123 | render_state(state);
124 | update_ui_undo_redo(state);
125 | }
126 |
127 | static void action_toggle_painting_panel(struct swappy_state *state,
128 | gboolean *toggled) {
129 | state->ui->panel_toggled =
130 | (toggled == NULL) ? !state->ui->panel_toggled : *toggled;
131 | update_ui_panel_toggle_button(state);
132 | }
133 |
134 | static void action_update_color_state(struct swappy_state *state, double r,
135 | double g, double b, double a,
136 | gboolean custom) {
137 | state->settings.r = r;
138 | state->settings.g = g;
139 | state->settings.b = b;
140 | state->settings.a = a;
141 |
142 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->color), custom);
143 | }
144 |
145 | static void action_set_color_from_custom(struct swappy_state *state) {
146 | GdkRGBA color;
147 | gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(state->ui->color), &color);
148 |
149 | action_update_color_state(state, color.red, color.green, color.blue,
150 | color.alpha, true);
151 | }
152 |
153 | static void switch_mode_to_brush(struct swappy_state *state) {
154 | state->mode = SWAPPY_PAINT_MODE_BRUSH;
155 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
156 | }
157 |
158 | static void switch_mode_to_text(struct swappy_state *state) {
159 | state->mode = SWAPPY_PAINT_MODE_TEXT;
160 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
161 | }
162 |
163 | static void switch_mode_to_rectangle(struct swappy_state *state) {
164 | state->mode = SWAPPY_PAINT_MODE_RECTANGLE;
165 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true);
166 | }
167 |
168 | static void switch_mode_to_ellipse(struct swappy_state *state) {
169 | state->mode = SWAPPY_PAINT_MODE_ELLIPSE;
170 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true);
171 | }
172 |
173 | static void switch_mode_to_arrow(struct swappy_state *state) {
174 | state->mode = SWAPPY_PAINT_MODE_ARROW;
175 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
176 | }
177 |
178 | static void switch_mode_to_blur(struct swappy_state *state) {
179 | state->mode = SWAPPY_PAINT_MODE_BLUR;
180 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
181 | }
182 |
183 | static void action_stroke_size_decrease(struct swappy_state *state) {
184 | guint step = state->settings.w <= 10 ? 1 : 5;
185 |
186 | state->settings.w -= step;
187 |
188 | if (state->settings.w < SWAPPY_LINE_SIZE_MIN) {
189 | state->settings.w = SWAPPY_LINE_SIZE_MIN;
190 | }
191 |
192 | update_ui_stroke_size_widget(state);
193 | }
194 |
195 | static void action_stroke_size_reset(struct swappy_state *state) {
196 | state->settings.w = state->config->line_size;
197 |
198 | update_ui_stroke_size_widget(state);
199 | }
200 |
201 | static void action_stroke_size_increase(struct swappy_state *state) {
202 | guint step = state->settings.w >= 10 ? 5 : 1;
203 | state->settings.w += step;
204 |
205 | if (state->settings.w > SWAPPY_LINE_SIZE_MAX) {
206 | state->settings.w = SWAPPY_LINE_SIZE_MAX;
207 | }
208 |
209 | update_ui_stroke_size_widget(state);
210 | }
211 |
212 | static void action_text_size_decrease(struct swappy_state *state) {
213 | guint step = state->settings.t <= 20 ? 1 : 5;
214 | state->settings.t -= step;
215 |
216 | if (state->settings.t < SWAPPY_TEXT_SIZE_MIN) {
217 | state->settings.t = SWAPPY_TEXT_SIZE_MIN;
218 | }
219 |
220 | update_ui_text_size_widget(state);
221 | }
222 | static void action_text_size_reset(struct swappy_state *state) {
223 | state->settings.t = state->config->text_size;
224 | update_ui_text_size_widget(state);
225 | }
226 | static void action_text_size_increase(struct swappy_state *state) {
227 | guint step = state->settings.t >= 20 ? 5 : 1;
228 | state->settings.t += step;
229 |
230 | if (state->settings.t > SWAPPY_TEXT_SIZE_MAX) {
231 | state->settings.t = SWAPPY_TEXT_SIZE_MAX;
232 | }
233 |
234 | update_ui_text_size_widget(state);
235 | }
236 |
237 | static void action_transparency_decrease(struct swappy_state *state) {
238 | state->settings.tr -= 10;
239 |
240 | if (state->settings.tr < SWAPPY_TRANSPARENCY_MIN) {
241 | state->settings.tr = SWAPPY_TRANSPARENCY_MIN;
242 | } else {
243 | // ceil to 10
244 | state->settings.tr += 5;
245 | state->settings.tr /= 10;
246 | state->settings.tr *= 10;
247 | }
248 |
249 | update_ui_transparency_widget(state);
250 | }
251 | static void action_transparency_reset(struct swappy_state *state) {
252 | state->settings.tr = state->config->transparency;
253 | update_ui_transparency_widget(state);
254 | }
255 | static void action_transparency_increase(struct swappy_state *state) {
256 | state->settings.tr += 10;
257 |
258 | if (state->settings.tr > SWAPPY_TRANSPARENCY_MAX) {
259 | state->settings.tr = SWAPPY_TRANSPARENCY_MAX;
260 | } else {
261 | // floor to 10
262 | state->settings.tr /= 10;
263 | state->settings.tr *= 10;
264 | }
265 |
266 | update_ui_transparency_widget(state);
267 | }
268 |
269 | static void action_fill_shape_toggle(struct swappy_state *state,
270 | gboolean *toggled) {
271 | // Don't allow changing the state via a shortcut if the button can't be
272 | // clicked.
273 | if (!gtk_widget_get_sensitive(GTK_WIDGET(state->ui->fill_shape))) return;
274 |
275 | gboolean toggle = (toggled == NULL) ? !state->config->fill_shape : *toggled;
276 | state->config->fill_shape = toggle;
277 |
278 | update_ui_fill_shape_toggle_button(state);
279 | }
280 |
281 | static void action_transparent_toggle(struct swappy_state *state,
282 | gboolean *toggled) {
283 | gboolean toggle = (toggled == NULL) ? !state->config->transparent : *toggled;
284 | state->config->transparent = toggle;
285 |
286 | update_ui_transparent_toggle_button(state);
287 | }
288 |
289 | static void save_state_to_file_or_folder(struct swappy_state *state,
290 | char *file) {
291 | GdkPixbuf *pixbuf = pixbuf_get_from_state(state);
292 |
293 | if (file == NULL) {
294 | pixbuf_save_state_to_folder(pixbuf, state->config->save_dir,
295 | state->config->save_filename_format);
296 | } else {
297 | pixbuf_save_to_file(pixbuf, file);
298 | }
299 |
300 | g_object_unref(pixbuf);
301 |
302 | if (state->config->early_exit) {
303 | gtk_main_quit();
304 | }
305 | }
306 |
307 | // We might need to save twice, once for auto_save config
308 | // and one for the output_file from -o CLI option.
309 | static void maybe_save_output_file(struct swappy_state *state) {
310 | if (state->config->auto_save) {
311 | save_state_to_file_or_folder(state, NULL);
312 | }
313 |
314 | if (state->output_file) {
315 | save_state_to_file_or_folder(state, state->output_file);
316 | }
317 | }
318 |
319 | static void screen_coordinates_to_image_coordinates(struct swappy_state *state,
320 | gdouble screen_x,
321 | gdouble screen_y,
322 | gdouble *image_x,
323 | gdouble *image_y) {
324 | gdouble x, y;
325 |
326 | gint w = gdk_pixbuf_get_width(state->original_image);
327 | gint h = gdk_pixbuf_get_height(state->original_image);
328 |
329 | // Clamp coordinates to original image properties to avoid side effects in
330 | // rendering pipeline
331 | x = CLAMP(screen_x / state->scaling_factor, 0, w);
332 | y = CLAMP(screen_y / state->scaling_factor, 0, h);
333 |
334 | *image_x = x;
335 | *image_y = y;
336 | }
337 |
338 | static void commit_state(struct swappy_state *state) {
339 | paint_commit_temporary(state);
340 | paint_free_list(&state->redo_paints);
341 | render_state(state);
342 | update_ui_undo_redo(state);
343 | }
344 |
345 | void on_destroy(GtkApplication *application, gpointer data) {
346 | struct swappy_state *state = (struct swappy_state *)data;
347 | maybe_save_output_file(state);
348 | }
349 |
350 | void brush_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
351 | switch_mode_to_brush(state);
352 | }
353 |
354 | void text_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
355 | switch_mode_to_text(state);
356 | }
357 |
358 | void rectangle_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
359 | switch_mode_to_rectangle(state);
360 | }
361 |
362 | void ellipse_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
363 | switch_mode_to_ellipse(state);
364 | }
365 |
366 | void arrow_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
367 | switch_mode_to_arrow(state);
368 | }
369 |
370 | void blur_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
371 | switch_mode_to_blur(state);
372 | }
373 |
374 | void save_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
375 | // Commit a potential paint (e.g. text being written)
376 | commit_state(state);
377 | save_state_to_file_or_folder(state, NULL);
378 | }
379 |
380 | void clear_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
381 | action_clear(state);
382 | }
383 |
384 | void copy_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
385 | // Commit a potential paint (e.g. text being written)
386 | commit_state(state);
387 | clipboard_copy_drawing_area_to_selection(state);
388 | }
389 |
390 | void control_modifier_changed(bool pressed, struct swappy_state *state) {
391 | if (state->temp_paint != NULL) {
392 | switch (state->temp_paint->type) {
393 | case SWAPPY_PAINT_MODE_ELLIPSE:
394 | case SWAPPY_PAINT_MODE_RECTANGLE:
395 | paint_update_temporary_shape(
396 | state, state->temp_paint->content.shape.to.x,
397 | state->temp_paint->content.shape.to.y, pressed);
398 | render_state(state);
399 | break;
400 | default:
401 | break;
402 | }
403 | }
404 | }
405 |
406 | static void im_context_commit(GtkIMContext *imc, gchar *str,
407 | gpointer user_data) {
408 | struct swappy_state *state = (struct swappy_state *)(user_data);
409 | if (state->temp_paint && state->mode == SWAPPY_PAINT_MODE_TEXT) {
410 | paint_update_temporary_str(state, str);
411 | render_state(state);
412 | return;
413 | }
414 | }
415 |
416 | static void clipboard_paste_selection(struct swappy_state *state) {
417 | GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
418 | gchar *text = gtk_clipboard_wait_for_text(clipboard);
419 | if (text) {
420 | paint_update_temporary_str(state, text);
421 | g_free(text);
422 | }
423 | }
424 |
425 | void window_keypress_handler(GtkWidget *widget, GdkEventKey *event,
426 | struct swappy_state *state) {
427 | if (state->temp_paint && state->mode == SWAPPY_PAINT_MODE_TEXT) {
428 | /* ctrl-v: paste */
429 | if (event->state & GDK_CONTROL_MASK && event->keyval == GDK_KEY_v) {
430 | clipboard_paste_selection(state);
431 | } else {
432 | paint_update_temporary_text(state, event);
433 | }
434 | render_state(state);
435 | return;
436 | }
437 | if (event->state & GDK_CONTROL_MASK) {
438 | switch (event->keyval) {
439 | case GDK_KEY_c:
440 | clipboard_copy_drawing_area_to_selection(state);
441 | break;
442 | case GDK_KEY_s:
443 | save_state_to_file_or_folder(state, NULL);
444 | break;
445 | case GDK_KEY_b:
446 | action_toggle_painting_panel(state, NULL);
447 | break;
448 | case GDK_KEY_w:
449 | gtk_main_quit();
450 | break;
451 | case GDK_KEY_z:
452 | action_undo(state);
453 | break;
454 | case GDK_KEY_Z:
455 | case GDK_KEY_y:
456 | action_redo(state);
457 | break;
458 | default:
459 | break;
460 | }
461 | } else {
462 | switch (event->keyval) {
463 | case GDK_KEY_Escape:
464 | case GDK_KEY_q:
465 | maybe_save_output_file(state);
466 | gtk_main_quit();
467 | break;
468 | case GDK_KEY_b:
469 | switch_mode_to_brush(state);
470 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->brush), true);
471 | break;
472 | case GDK_KEY_e:
473 | case GDK_KEY_t:
474 | switch_mode_to_text(state);
475 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->text), true);
476 | break;
477 | case GDK_KEY_s:
478 | case GDK_KEY_r:
479 | switch_mode_to_rectangle(state);
480 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->rectangle),
481 | true);
482 | break;
483 | case GDK_KEY_c:
484 | case GDK_KEY_o:
485 | switch_mode_to_ellipse(state);
486 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->ellipse),
487 | true);
488 | break;
489 | case GDK_KEY_a:
490 | switch_mode_to_arrow(state);
491 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->arrow), true);
492 | break;
493 | case GDK_KEY_d:
494 | switch_mode_to_blur(state);
495 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blur), true);
496 | break;
497 | case GDK_KEY_x:
498 | case GDK_KEY_k:
499 | action_clear(state);
500 | break;
501 | case GDK_KEY_R:
502 | action_update_color_state(state, 1, 0, 0, 1, false);
503 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->red), true);
504 | break;
505 | case GDK_KEY_G:
506 | action_update_color_state(state, 0, 1, 0, 1, false);
507 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->green), true);
508 | break;
509 | case GDK_KEY_B:
510 | action_update_color_state(state, 0, 0, 1, 1, false);
511 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blue), true);
512 | break;
513 | case GDK_KEY_C:
514 | action_set_color_from_custom(state);
515 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->custom),
516 | true);
517 | break;
518 | case GDK_KEY_minus:
519 | action_stroke_size_decrease(state);
520 | break;
521 | case GDK_KEY_equal:
522 | action_stroke_size_reset(state);
523 | break;
524 | case GDK_KEY_plus:
525 | action_stroke_size_increase(state);
526 | break;
527 | case GDK_KEY_Control_L:
528 | control_modifier_changed(true, state);
529 | break;
530 | case GDK_KEY_f:
531 | action_fill_shape_toggle(state, NULL);
532 | break;
533 | case GDK_KEY_T:
534 | action_transparent_toggle(state, NULL);
535 | break;
536 | default:
537 | break;
538 | }
539 | }
540 | }
541 |
542 | void window_keyrelease_handler(GtkWidget *widget, GdkEventKey *event,
543 | struct swappy_state *state) {
544 | if (event->state & GDK_CONTROL_MASK) {
545 | switch (event->keyval) {
546 | case GDK_KEY_Control_L:
547 | control_modifier_changed(false, state);
548 | break;
549 | default:
550 | break;
551 | }
552 | } else {
553 | switch (event->keyval) {
554 | default:
555 | break;
556 | }
557 | }
558 | }
559 |
560 | gboolean window_delete_handler(GtkWidget *widget, GdkEvent *event,
561 | struct swappy_state *state) {
562 | gtk_main_quit();
563 | return FALSE;
564 | }
565 |
566 | void pane_toggled_handler(GtkWidget *widget, struct swappy_state *state) {
567 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget);
568 | gboolean toggled = gtk_toggle_button_get_active(button);
569 | action_toggle_painting_panel(state, &toggled);
570 | }
571 |
572 | void undo_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
573 | action_undo(state);
574 | }
575 |
576 | void redo_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
577 | action_redo(state);
578 | }
579 |
580 | gboolean draw_area_handler(GtkWidget *widget, cairo_t *cr,
581 | struct swappy_state *state) {
582 | GtkAllocation *alloc = g_new(GtkAllocation, 1);
583 | gtk_widget_get_allocation(widget, alloc);
584 |
585 | GdkPixbuf *image = state->original_image;
586 | gint image_width = gdk_pixbuf_get_width(image);
587 | gint image_height = gdk_pixbuf_get_height(image);
588 | double scale_x = (double)alloc->width / image_width;
589 | double scale_y = (double)alloc->height / image_height;
590 |
591 | cairo_scale(cr, scale_x, scale_y);
592 | cairo_set_source_surface(cr, state->rendering_surface, 0, 0);
593 | cairo_paint(cr);
594 |
595 | return FALSE;
596 | }
597 |
598 | gboolean draw_area_configure_handler(GtkWidget *widget,
599 | GdkEventConfigure *event,
600 | struct swappy_state *state) {
601 | g_debug("received configure_event callback");
602 |
603 | pixbuf_scale_surface_from_widget(state, widget);
604 |
605 | render_state(state);
606 |
607 | return TRUE;
608 | }
609 |
610 | void draw_area_button_press_handler(GtkWidget *widget, GdkEventButton *event,
611 | struct swappy_state *state) {
612 | gdouble x, y;
613 |
614 | screen_coordinates_to_image_coordinates(state, event->x, event->y, &x, &y);
615 |
616 | if (event->button == 1) {
617 | switch (state->mode) {
618 | case SWAPPY_PAINT_MODE_BLUR:
619 | case SWAPPY_PAINT_MODE_BRUSH:
620 | case SWAPPY_PAINT_MODE_RECTANGLE:
621 | case SWAPPY_PAINT_MODE_ELLIPSE:
622 | case SWAPPY_PAINT_MODE_ARROW:
623 | case SWAPPY_PAINT_MODE_TEXT:
624 | paint_add_temporary(state, x, y, state->mode);
625 | render_state(state);
626 | update_ui_undo_redo(state);
627 | break;
628 | default:
629 | return;
630 | }
631 | }
632 | }
633 | void draw_area_motion_notify_handler(GtkWidget *widget, GdkEventMotion *event,
634 | struct swappy_state *state) {
635 | gdouble x, y;
636 |
637 | screen_coordinates_to_image_coordinates(state, event->x, event->y, &x, &y);
638 |
639 | GdkDisplay *display = gdk_display_get_default();
640 | GdkWindow *window = event->window;
641 | GdkCursor *crosshair = gdk_cursor_new_for_display(display, GDK_CROSSHAIR);
642 | gdk_window_set_cursor(window, crosshair);
643 |
644 | gboolean is_button1_pressed = event->state & GDK_BUTTON1_MASK;
645 | gboolean is_control_pressed = event->state & GDK_CONTROL_MASK;
646 |
647 | switch (state->mode) {
648 | case SWAPPY_PAINT_MODE_BLUR:
649 | case SWAPPY_PAINT_MODE_BRUSH:
650 | case SWAPPY_PAINT_MODE_RECTANGLE:
651 | case SWAPPY_PAINT_MODE_ELLIPSE:
652 | case SWAPPY_PAINT_MODE_ARROW:
653 | if (is_button1_pressed) {
654 | paint_update_temporary_shape(state, x, y, is_control_pressed);
655 | render_state(state);
656 | }
657 | break;
658 | case SWAPPY_PAINT_MODE_TEXT:
659 | if (is_button1_pressed) {
660 | paint_update_temporary_text_clip(state, x, y);
661 | render_state(state);
662 | }
663 | break;
664 | default:
665 | return;
666 | }
667 | g_object_unref(crosshair);
668 | }
669 | void draw_area_button_release_handler(GtkWidget *widget, GdkEventButton *event,
670 | struct swappy_state *state) {
671 | if (!(event->state & GDK_BUTTON1_MASK)) {
672 | return;
673 | }
674 |
675 | switch (state->mode) {
676 | case SWAPPY_PAINT_MODE_BLUR:
677 | case SWAPPY_PAINT_MODE_BRUSH:
678 | case SWAPPY_PAINT_MODE_RECTANGLE:
679 | case SWAPPY_PAINT_MODE_ELLIPSE:
680 | case SWAPPY_PAINT_MODE_ARROW:
681 | commit_state(state);
682 | break;
683 | case SWAPPY_PAINT_MODE_TEXT:
684 | if (state->temp_paint && !state->temp_paint->can_draw) {
685 | paint_free(state->temp_paint);
686 | state->temp_paint = NULL;
687 | }
688 | break;
689 | default:
690 | return;
691 | }
692 | }
693 |
694 | void color_red_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
695 | action_update_color_state(state, 1, 0, 0, 1, false);
696 | }
697 |
698 | void color_green_clicked_handler(GtkWidget *widget,
699 | struct swappy_state *state) {
700 | action_update_color_state(state, 0, 1, 0, 1, false);
701 | }
702 |
703 | void color_blue_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
704 | action_update_color_state(state, 0, 0, 1, 1, false);
705 | }
706 |
707 | void color_custom_clicked_handler(GtkWidget *widget,
708 | struct swappy_state *state) {
709 | action_set_color_from_custom(state);
710 | }
711 |
712 | void color_custom_color_set_handler(GtkWidget *widget,
713 | struct swappy_state *state) {
714 | action_set_color_from_custom(state);
715 | }
716 |
717 | void stroke_size_decrease_handler(GtkWidget *widget,
718 | struct swappy_state *state) {
719 | action_stroke_size_decrease(state);
720 | }
721 |
722 | void stroke_size_reset_handler(GtkWidget *widget, struct swappy_state *state) {
723 | action_stroke_size_reset(state);
724 | }
725 | void stroke_size_increase_handler(GtkWidget *widget,
726 | struct swappy_state *state) {
727 | action_stroke_size_increase(state);
728 | }
729 |
730 | void text_size_decrease_handler(GtkWidget *widget, struct swappy_state *state) {
731 | action_text_size_decrease(state);
732 | }
733 | void text_size_reset_handler(GtkWidget *widget, struct swappy_state *state) {
734 | action_text_size_reset(state);
735 | }
736 | void text_size_increase_handler(GtkWidget *widget, struct swappy_state *state) {
737 | action_text_size_increase(state);
738 | }
739 |
740 | void transparency_decrease_handler(GtkWidget *widget,
741 | struct swappy_state *state) {
742 | action_transparency_decrease(state);
743 | }
744 | void transparency_reset_handler(GtkWidget *widget, struct swappy_state *state) {
745 | action_transparency_reset(state);
746 | }
747 | void transparency_increase_handler(GtkWidget *widget,
748 | struct swappy_state *state) {
749 | action_transparency_increase(state);
750 | }
751 |
752 | void fill_shape_toggled_handler(GtkWidget *widget, struct swappy_state *state) {
753 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget);
754 | gboolean toggled = gtk_toggle_button_get_active(button);
755 | action_fill_shape_toggle(state, &toggled);
756 | }
757 |
758 | void transparent_toggled_handler(GtkWidget *widget,
759 | struct swappy_state *state) {
760 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget);
761 | gboolean toggled = gtk_toggle_button_get_active(button);
762 | action_transparent_toggle(state, &toggled);
763 | }
764 |
765 | static void compute_window_size_and_scaling_factor(struct swappy_state *state) {
766 | GdkRectangle workarea = {0};
767 | GdkDisplay *display = gdk_display_get_default();
768 | GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(state->ui->window));
769 | GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window);
770 | gdk_monitor_get_workarea(monitor, &workarea);
771 |
772 | g_assert(workarea.width > 0);
773 | g_assert(workarea.height > 0);
774 |
775 | if (state->window) {
776 | g_free(state->window);
777 | state->window = NULL;
778 | }
779 |
780 | state->window = g_new(struct swappy_box, 1);
781 | state->window->x = workarea.x;
782 | state->window->y = workarea.y;
783 |
784 | double threshold = 0.75;
785 | double scaling_factor = 1.0;
786 |
787 | int image_width = gdk_pixbuf_get_width(state->original_image);
788 | int image_height = gdk_pixbuf_get_height(state->original_image);
789 |
790 | int max_width = workarea.width * threshold;
791 | int max_height = workarea.height * threshold;
792 |
793 | g_info("size of image: %ux%u", image_width, image_height);
794 | g_info("size of monitor at window: %ux%u", workarea.width, workarea.height);
795 | g_info("maxium size allowed for window: %ux%u", max_width, max_height);
796 |
797 | int scaled_width = image_width;
798 | int scaled_height = image_height;
799 |
800 | double scaling_factor_width = (double)max_width / image_width;
801 | double scaling_factor_height = (double)max_height / image_height;
802 |
803 | if (scaling_factor_height < 1.0 || scaling_factor_width < 1.0) {
804 | scaling_factor = MIN(scaling_factor_width, scaling_factor_height);
805 | scaled_width = image_width * scaling_factor;
806 | scaled_height = image_height * scaling_factor;
807 | g_info("rendering area will be scaled by a factor of: %.2lf",
808 | scaling_factor);
809 | }
810 |
811 | state->scaling_factor = scaling_factor;
812 | state->window->width = scaled_width;
813 | state->window->height = scaled_height;
814 |
815 | g_info("size of window to render: %ux%u", state->window->width,
816 | state->window->height);
817 | }
818 |
819 | static void apply_css(GtkWidget *widget, GtkStyleProvider *provider) {
820 | gtk_style_context_add_provider(gtk_widget_get_style_context(widget), provider,
821 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
822 | if (GTK_IS_CONTAINER(widget)) {
823 | gtk_container_forall(GTK_CONTAINER(widget), (GtkCallback)apply_css,
824 | provider);
825 | }
826 | }
827 |
828 | static bool load_css(struct swappy_state *state) {
829 | GtkCssProvider *provider = gtk_css_provider_new();
830 | gtk_css_provider_load_from_resource(provider,
831 | "/me/jtheoof/swappy/style/swappy.css");
832 | apply_css(GTK_WIDGET(state->ui->window), GTK_STYLE_PROVIDER(provider));
833 | g_object_unref(provider);
834 | return true;
835 | }
836 |
837 | static bool load_layout(struct swappy_state *state) {
838 | GError *error = NULL;
839 | // init color
840 | GdkRGBA color;
841 |
842 | /* Construct a GtkBuilder instance and load our UI description */
843 | GtkBuilder *builder = gtk_builder_new();
844 |
845 | // Set translation domain for the application based on `src/po/meson.build`
846 | gtk_builder_set_translation_domain(builder, GETTEXT_PACKAGE);
847 |
848 | if (gtk_builder_add_from_resource(builder, "/me/jtheoof/swappy/swappy.glade",
849 | &error) == 0) {
850 | g_printerr("Error loading file: %s", error->message);
851 | g_clear_error(&error);
852 | return false;
853 | }
854 |
855 | gtk_builder_connect_signals(builder, state);
856 |
857 | GtkWindow *window =
858 | GTK_WINDOW(gtk_builder_get_object(builder, "paint-window"));
859 | GtkIMContext *im_context = gtk_im_multicontext_new();
860 | gtk_im_context_set_client_window(im_context,
861 | gtk_widget_get_window(GTK_WIDGET(window)));
862 | g_signal_connect(G_OBJECT(im_context), "commit",
863 | G_CALLBACK(im_context_commit), state);
864 | state->ui->im_context = im_context;
865 |
866 | g_signal_connect(window, "destroy", G_CALLBACK(on_destroy), state);
867 |
868 | state->ui->panel_toggle_button =
869 | GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "btn-toggle-panel"));
870 |
871 | state->ui->undo = GTK_BUTTON(gtk_builder_get_object(builder, "undo-button"));
872 | state->ui->redo = GTK_BUTTON(gtk_builder_get_object(builder, "redo-button"));
873 |
874 | GtkWidget *area =
875 | GTK_WIDGET(gtk_builder_get_object(builder, "painting-area"));
876 |
877 | state->ui->painting_box =
878 | GTK_BOX(gtk_builder_get_object(builder, "painting-box"));
879 | GtkRadioButton *brush =
880 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "brush"));
881 | GtkRadioButton *text =
882 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "text"));
883 | GtkRadioButton *rectangle =
884 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "rectangle"));
885 | GtkRadioButton *ellipse =
886 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "ellipse"));
887 | GtkRadioButton *arrow =
888 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "arrow"));
889 | GtkRadioButton *blur =
890 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "blur"));
891 |
892 | state->ui->red =
893 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-red-button"));
894 | state->ui->green =
895 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-green-button"));
896 | state->ui->blue =
897 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-blue-button"));
898 | state->ui->custom =
899 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-custom-button"));
900 | state->ui->color =
901 | GTK_COLOR_BUTTON(gtk_builder_get_object(builder, "custom-color-button"));
902 |
903 | state->ui->line_size =
904 | GTK_BUTTON(gtk_builder_get_object(builder, "stroke-size-button"));
905 | state->ui->text_size =
906 | GTK_BUTTON(gtk_builder_get_object(builder, "text-size-button"));
907 | state->ui->transparency =
908 | GTK_BUTTON(gtk_builder_get_object(builder, "transparency-button"));
909 | state->ui->transparency_plus =
910 | GTK_BUTTON(gtk_builder_get_object(builder, "transparency-plus-button"));
911 | state->ui->transparency_minus =
912 | GTK_BUTTON(gtk_builder_get_object(builder, "transparency-minus-button"));
913 |
914 | state->ui->fill_shape = GTK_TOGGLE_BUTTON(
915 | gtk_builder_get_object(builder, "fill-shape-toggle-button"));
916 |
917 | gdk_rgba_parse(&color, state->config->custom_color);
918 | gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(state->ui->color), &color);
919 | state->ui->transparent = GTK_TOGGLE_BUTTON(
920 | gtk_builder_get_object(builder, "transparent-toggle-button"));
921 |
922 | state->ui->brush = brush;
923 | state->ui->text = text;
924 | state->ui->rectangle = rectangle;
925 | state->ui->ellipse = ellipse;
926 | state->ui->arrow = arrow;
927 | state->ui->blur = blur;
928 | state->ui->area = area;
929 | state->ui->window = window;
930 |
931 | compute_window_size_and_scaling_factor(state);
932 | gtk_widget_set_size_request(area, state->window->width,
933 | state->window->height);
934 | action_toggle_painting_panel(state, &state->config->show_panel);
935 |
936 | g_object_unref(G_OBJECT(builder));
937 |
938 | return true;
939 | }
940 |
941 | static void set_paint_mode(struct swappy_state *state) {
942 | switch (state->mode) {
943 | case SWAPPY_PAINT_MODE_BRUSH:
944 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->brush), true);
945 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
946 | break;
947 | case SWAPPY_PAINT_MODE_TEXT:
948 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->text), true);
949 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
950 | break;
951 | case SWAPPY_PAINT_MODE_RECTANGLE:
952 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->rectangle),
953 | true);
954 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true);
955 | break;
956 | case SWAPPY_PAINT_MODE_ELLIPSE:
957 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->ellipse), true);
958 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true);
959 | break;
960 | case SWAPPY_PAINT_MODE_ARROW:
961 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->arrow), true);
962 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
963 | break;
964 | case SWAPPY_PAINT_MODE_BLUR:
965 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blur), true);
966 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
967 | break;
968 | default:
969 | break;
970 | }
971 | }
972 |
973 | static bool init_gtk_window(struct swappy_state *state) {
974 | if (!state->original_image) {
975 | g_critical("original image not loaded");
976 | return false;
977 | }
978 |
979 | if (!load_layout(state)) {
980 | return false;
981 | }
982 |
983 | if (!load_css(state)) {
984 | return false;
985 | }
986 |
987 | set_paint_mode(state);
988 |
989 | update_ui_stroke_size_widget(state);
990 | update_ui_text_size_widget(state);
991 | update_ui_transparency_widget(state);
992 | update_ui_undo_redo(state);
993 | update_ui_panel_toggle_button(state);
994 | update_ui_fill_shape_toggle_button(state);
995 | update_ui_transparent_toggle_button(state);
996 |
997 | return true;
998 | }
999 |
1000 | static gboolean has_option_file(struct swappy_state *state) {
1001 | return (state->file_str != NULL);
1002 | }
1003 |
1004 | static gboolean is_file_from_stdin(const char *file) {
1005 | return (strcmp(file, "-") == 0);
1006 | }
1007 |
1008 | static void init_settings(struct swappy_state *state) {
1009 | state->settings.r = 1;
1010 | state->settings.g = 0;
1011 | state->settings.b = 0;
1012 | state->settings.a = 1;
1013 | state->settings.w = state->config->line_size;
1014 | state->settings.t = state->config->text_size;
1015 | state->settings.tr = state->config->transparency;
1016 | state->mode = state->config->paint_mode;
1017 | }
1018 |
1019 | static gint command_line_handler(GtkApplication *app,
1020 | GApplicationCommandLine *cmdline,
1021 | struct swappy_state *state) {
1022 | config_load(state);
1023 | init_settings(state);
1024 |
1025 | if (has_option_file(state)) {
1026 | if (is_file_from_stdin(state->file_str)) {
1027 | char *temp_file_str = file_dump_stdin_into_a_temp_file();
1028 | state->temp_file_str = temp_file_str;
1029 | }
1030 |
1031 | if (!pixbuf_init_from_file(state)) {
1032 | return EXIT_FAILURE;
1033 | }
1034 | }
1035 |
1036 | if (!init_gtk_window(state)) {
1037 | return EXIT_FAILURE;
1038 | }
1039 |
1040 | return EXIT_SUCCESS;
1041 | }
1042 |
1043 | // Print version and quit
1044 | gboolean callback_on_flag(const gchar *option_name, const gchar *value,
1045 | gpointer data, GError **error) {
1046 | if (!strcmp(option_name, "-v") || !strcmp(option_name, "--version")) {
1047 | printf("swappy version %s\n", SWAPPY_VERSION);
1048 | exit(0);
1049 | }
1050 | return TRUE;
1051 | }
1052 |
1053 | bool application_init(struct swappy_state *state) {
1054 | // Callback function for flags
1055 | gboolean (*GOptionArgFunc)(const gchar *option_name, const gchar *value,
1056 | gpointer data, GError **error);
1057 | GOptionArgFunc = &callback_on_flag;
1058 |
1059 | const GOptionEntry cli_options[] = {
1060 | {
1061 | .long_name = "file",
1062 | .short_name = 'f',
1063 | .arg = G_OPTION_ARG_STRING,
1064 | .arg_data = &state->file_str,
1065 | .description = "Load a file at a specific path",
1066 | },
1067 | {
1068 | .long_name = "output-file",
1069 | .short_name = 'o',
1070 | .arg = G_OPTION_ARG_STRING,
1071 | .arg_data = &state->output_file,
1072 | .description = "Print the final surface to the given file when "
1073 | "exiting, use - to print to stdout",
1074 | },
1075 | {
1076 | .long_name = "version",
1077 | .short_name = 'v',
1078 | .flags = G_OPTION_FLAG_NO_ARG,
1079 | .arg = G_OPTION_ARG_CALLBACK,
1080 | .arg_data = GOptionArgFunc,
1081 | .description = "Print version and quit",
1082 | },
1083 | {NULL}}; // NOLINT(clang-diagnostic-missing-field-initializers)
1084 |
1085 | state->app = gtk_application_new("me.jtheoof.swappy",
1086 | G_APPLICATION_HANDLES_COMMAND_LINE);
1087 |
1088 | if (state->app == NULL) {
1089 | g_critical("cannot create gtk application");
1090 | return false;
1091 | }
1092 |
1093 | g_application_add_main_option_entries(G_APPLICATION(state->app), cli_options);
1094 |
1095 | state->ui = g_new(struct swappy_state_ui, 1);
1096 | state->ui->panel_toggled = false;
1097 |
1098 | g_signal_connect(state->app, "command-line", G_CALLBACK(command_line_handler),
1099 | state);
1100 |
1101 | return true;
1102 | }
1103 |
1104 | int application_run(struct swappy_state *state) {
1105 | return g_application_run(G_APPLICATION(state->app), state->argc, state->argv);
1106 | }
1107 |
--------------------------------------------------------------------------------
/src/box.c:
--------------------------------------------------------------------------------
1 | #include "box.h"
2 |
3 | static int32_t lmax(int32_t a, int32_t b) { return a > b ? a : b; }
4 |
5 | static int32_t lmin(int32_t a, int32_t b) { return a < b ? a : b; }
6 |
7 | bool box_parse(struct swappy_box *box, const char *str) {
8 | char *end = NULL;
9 | box->x = (int32_t)strtol(str, &end, 10);
10 | if (end[0] != ',') {
11 | return false;
12 | }
13 |
14 | char *next = end + 1;
15 | box->y = (int32_t)strtol(next, &end, 10);
16 | if (end[0] != ' ') {
17 | return false;
18 | }
19 |
20 | next = end + 1;
21 | box->width = (int32_t)strtol(next, &end, 10);
22 | if (end[0] != 'x') {
23 | return false;
24 | }
25 |
26 | next = end + 1;
27 | box->height = (int32_t)strtol(next, &end, 10);
28 | if (end[0] != '\0') {
29 | return false;
30 | }
31 |
32 | return true;
33 | }
34 |
35 | bool is_empty_box(struct swappy_box *box) {
36 | return box->width <= 0 || box->height <= 0;
37 | }
38 |
39 | bool intersect_box(struct swappy_box *a, struct swappy_box *b) {
40 | if (is_empty_box(a) || is_empty_box(b)) {
41 | return false;
42 | }
43 |
44 | int32_t x1 = lmax(a->x, b->x);
45 | int32_t y1 = lmax(a->y, b->y);
46 | int32_t x2 = lmin(a->x + a->width, b->x + b->width);
47 | int32_t y2 = lmin(a->y + a->height, b->y + b->height);
48 |
49 | struct swappy_box box = {
50 | .x = x1,
51 | .y = y1,
52 | .width = x2 - x1,
53 | .height = y2 - y1,
54 | };
55 | return !is_empty_box(&box);
56 | }
57 |
--------------------------------------------------------------------------------
/src/clipboard.c:
--------------------------------------------------------------------------------
1 | #include "clipboard.h"
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | #include "pixbuf.h"
8 | #include "util.h"
9 |
10 | #define gtk_clipboard_t GtkClipboard
11 | #define gdk_pixbuf_t GdkPixbuf
12 |
13 | static gboolean send_pixbuf_to_wl_copy(gdk_pixbuf_t *pixbuf) {
14 | pid_t clipboard_process = 0;
15 | int pipefd[2];
16 | int status;
17 | ssize_t written;
18 | gsize size;
19 | gchar *buffer = NULL;
20 | GError *error = NULL;
21 |
22 | if (pipe(pipefd) < 0) {
23 | g_warning("unable to pipe for copy process to work");
24 | return false;
25 | }
26 | clipboard_process = fork();
27 | if (clipboard_process == -1) {
28 | g_warning("unable to fork process for copy");
29 | return false;
30 | }
31 | if (clipboard_process == 0) {
32 | close(pipefd[1]);
33 | dup2(pipefd[0], STDIN_FILENO);
34 | close(pipefd[0]);
35 | execlp("wl-copy", "wl-copy", "-t", "image/png", NULL);
36 | g_warning(
37 | "Unable to copy contents to clipboard. Please make sure you have "
38 | "`wl-clipboard`, `xclip`, or `xsel` installed.");
39 | exit(1);
40 | }
41 | close(pipefd[0]);
42 |
43 | gdk_pixbuf_save_to_buffer(pixbuf, &buffer, &size, "png", &error, NULL);
44 |
45 | if (error != NULL) {
46 | g_critical("unable to save pixbuf to buffer for copy: %s", error->message);
47 | g_error_free(error);
48 | return false;
49 | }
50 |
51 | written = write(pipefd[1], buffer, size);
52 | if (written == -1) {
53 | g_warning("unable to write to pipe fd for copy");
54 | g_free(buffer);
55 | return false;
56 | }
57 |
58 | close(pipefd[1]);
59 | g_free(buffer);
60 | waitpid(clipboard_process, &status, 0);
61 |
62 | if (WIFEXITED(status)) {
63 | return WEXITSTATUS(status) == 0; // Make sure the child exited properly
64 | }
65 |
66 | return false;
67 | }
68 |
69 | static void send_pixbuf_to_gdk_clipboard(gdk_pixbuf_t *pixbuf) {
70 | gtk_clipboard_t *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
71 | gtk_clipboard_set_image(clipboard, pixbuf);
72 | gtk_clipboard_store(clipboard); // Does not work for Wayland gdk backend
73 | }
74 |
75 | bool clipboard_copy_drawing_area_to_selection(struct swappy_state *state) {
76 | gdk_pixbuf_t *pixbuf = pixbuf_get_from_state(state);
77 |
78 | // Try `wl-copy` first and fall back to gtk function. See README.md.
79 | if (!send_pixbuf_to_wl_copy(pixbuf)) {
80 | send_pixbuf_to_gdk_clipboard(pixbuf);
81 | }
82 |
83 | g_object_unref(pixbuf);
84 |
85 | if (state->config->early_exit) {
86 | gtk_main_quit();
87 | }
88 |
89 | return true;
90 | }
91 |
--------------------------------------------------------------------------------
/src/config.c:
--------------------------------------------------------------------------------
1 | #include "config.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | #include "file.h"
11 | #include "swappy.h"
12 |
13 | static void print_config(struct swappy_config *config) {
14 | g_info("printing config:");
15 | g_info("config_dir: %s", config->config_file);
16 | g_info("save_dir: %s", config->save_dir);
17 | g_info("save_filename_format: %s", config->save_filename_format);
18 | g_info("show_panel: %d", config->show_panel);
19 | g_info("line_size: %d", config->line_size);
20 | g_info("text_font: %s", config->text_font);
21 | g_info("text_size: %d", config->text_size);
22 | g_info("transparency: %d", config->transparency);
23 | g_info("paint_mode: %d", config->paint_mode);
24 | g_info("early_exit: %d", config->early_exit);
25 | g_info("fill_shape: %d", config->fill_shape);
26 | g_info("auto_save: %d", config->auto_save);
27 | g_info("custom_color: %s", config->custom_color);
28 | g_info("transparent: %d", config->transparent);
29 | }
30 |
31 | static char *get_default_save_dir() {
32 | static const char *storage_paths[] = {
33 | "$XDG_DESKTOP_DIR",
34 | "$XDG_CONFIG_HOME/Desktop",
35 | "$HOME/Desktop",
36 | "$HOME",
37 | };
38 |
39 | for (size_t i = 0; i < sizeof(storage_paths) / sizeof(char *); ++i) {
40 | wordexp_t p;
41 | if (wordexp(storage_paths[i], &p, 0) == 0) {
42 | char *path = g_strdup(p.we_wordv[0]);
43 | wordfree(&p);
44 | if (path && folder_exists(path)) {
45 | return path;
46 | }
47 | g_free(path);
48 | }
49 | }
50 |
51 | return NULL;
52 | }
53 |
54 | static char *get_config_file() {
55 | static const char *storage_paths[] = {
56 | "$XDG_CONFIG_HOME/swappy/config",
57 | "$HOME/.config/swappy/config",
58 | };
59 |
60 | for (size_t i = 0; i < sizeof(storage_paths) / sizeof(char *); ++i) {
61 | wordexp_t p;
62 | if (wordexp(storage_paths[i], &p, 0) == 0) {
63 | char *path = g_strdup(p.we_wordv[0]);
64 | wordfree(&p);
65 | if (path && file_exists(path)) {
66 | return path;
67 | }
68 | g_free(path);
69 | }
70 | }
71 |
72 | return NULL;
73 | }
74 |
75 | static void load_config_from_file(struct swappy_config *config,
76 | const char *file) {
77 | GKeyFile *gkf;
78 | const gchar *group = "Default";
79 | gchar *save_dir = NULL;
80 | gchar *save_filename_format = NULL;
81 | gboolean show_panel;
82 | gchar *save_dir_expanded = NULL;
83 | guint64 line_size, text_size;
84 | guint64 transparency;
85 | gchar *text_font = NULL;
86 | gchar *paint_mode = NULL;
87 | gboolean early_exit;
88 | gboolean fill_shape;
89 | gboolean auto_save;
90 | gchar *custom_color = NULL;
91 | gboolean transparent;
92 | GError *error = NULL;
93 |
94 | if (file == NULL) {
95 | return;
96 | }
97 |
98 | gkf = g_key_file_new();
99 |
100 | if (!g_key_file_load_from_file(gkf, file, G_KEY_FILE_NONE, NULL)) {
101 | g_warning("could not read config file %s", file);
102 | g_key_file_free(gkf);
103 | return;
104 | }
105 |
106 | save_dir = g_key_file_get_string(gkf, group, "save_dir", &error);
107 |
108 | if (error == NULL) {
109 | wordexp_t p;
110 | if (wordexp(save_dir, &p, 0) == 0) {
111 | save_dir_expanded = g_strdup(p.we_wordv[0]);
112 | wordfree(&p);
113 | if (!save_dir_expanded || !folder_exists(save_dir_expanded)) {
114 | g_info("save_dir: attempting to create non-existent directory '%s'",
115 | save_dir_expanded);
116 | if (g_mkdir_with_parents(save_dir_expanded, 0755)) {
117 | g_warning("save_dir: failed to create '%s'", save_dir_expanded);
118 | }
119 | }
120 |
121 | g_free(save_dir);
122 | g_free(config->save_dir);
123 | config->save_dir = save_dir_expanded;
124 | }
125 | } else {
126 | g_info("save_dir is missing in %s (%s)", file, error->message);
127 | g_error_free(error);
128 | error = NULL;
129 | }
130 |
131 | save_filename_format =
132 | g_key_file_get_string(gkf, group, "save_filename_format", &error);
133 |
134 | if (error == NULL) {
135 | config->save_filename_format = save_filename_format;
136 | } else {
137 | g_info("save_filename_format is missing in %s (%s)", file, error->message);
138 | g_error_free(error);
139 | error = NULL;
140 | }
141 |
142 | line_size = g_key_file_get_uint64(gkf, group, "line_size", &error);
143 |
144 | if (error == NULL) {
145 | if (line_size >= SWAPPY_LINE_SIZE_MIN &&
146 | line_size <= SWAPPY_LINE_SIZE_MAX) {
147 | config->line_size = line_size;
148 | } else {
149 | g_warning("line_size is not a valid value: %" PRIu64
150 | " - see man page for details",
151 | line_size);
152 | }
153 | } else {
154 | g_info("line_size is missing in %s (%s)", file, error->message);
155 | g_error_free(error);
156 | error = NULL;
157 | }
158 |
159 | text_size = g_key_file_get_uint64(gkf, group, "text_size", &error);
160 |
161 | if (error == NULL) {
162 | if (text_size >= SWAPPY_TEXT_SIZE_MIN &&
163 | text_size <= SWAPPY_TEXT_SIZE_MAX) {
164 | config->text_size = text_size;
165 | } else {
166 | g_warning("text_size is not a valid value: %" PRIu64
167 | " - see man page for details",
168 | text_size);
169 | }
170 | } else {
171 | g_info("text_size is missing in %s (%s)", file, error->message);
172 | g_error_free(error);
173 | error = NULL;
174 | }
175 |
176 | text_font = g_key_file_get_string(gkf, group, "text_font", &error);
177 |
178 | if (error == NULL) {
179 | g_free(config->text_font);
180 | config->text_font = text_font;
181 | } else {
182 | g_info("text_font is missing in %s (%s)", file, error->message);
183 | g_error_free(error);
184 | error = NULL;
185 | }
186 |
187 | transparency = g_key_file_get_uint64(gkf, group, "transparency", &error);
188 |
189 | if (error == NULL) {
190 | if (transparency >= SWAPPY_TRANSPARENCY_MIN &&
191 | transparency <= SWAPPY_TRANSPARENCY_MAX) {
192 | config->transparency = transparency;
193 | } else {
194 | g_warning("transparency is not a valid value: %" PRIu64
195 | " - see man page for details",
196 | transparency);
197 | }
198 | } else {
199 | g_info("transparency is missing in %s (%s)", file, error->message);
200 | g_error_free(error);
201 | error = NULL;
202 | }
203 |
204 | show_panel = g_key_file_get_boolean(gkf, group, "show_panel", &error);
205 |
206 | if (error == NULL) {
207 | config->show_panel = show_panel;
208 | } else {
209 | g_info("show_panel is missing in %s (%s)", file, error->message);
210 | g_error_free(error);
211 | error = NULL;
212 | }
213 |
214 | early_exit = g_key_file_get_boolean(gkf, group, "early_exit", &error);
215 |
216 | if (error == NULL) {
217 | config->early_exit = early_exit;
218 | } else {
219 | g_info("early_exit is missing in %s (%s)", file, error->message);
220 | g_error_free(error);
221 | error = NULL;
222 | }
223 |
224 | paint_mode = g_key_file_get_string(gkf, group, "paint_mode", &error);
225 |
226 | if (error == NULL) {
227 | if (g_ascii_strcasecmp(paint_mode, "brush") == 0) {
228 | config->paint_mode = SWAPPY_PAINT_MODE_BRUSH;
229 | } else if (g_ascii_strcasecmp(paint_mode, "text") == 0) {
230 | config->paint_mode = SWAPPY_PAINT_MODE_TEXT;
231 | } else if (g_ascii_strcasecmp(paint_mode, "rectangle") == 0) {
232 | config->paint_mode = SWAPPY_PAINT_MODE_RECTANGLE;
233 | } else if (g_ascii_strcasecmp(paint_mode, "ellipse") == 0) {
234 | config->paint_mode = SWAPPY_PAINT_MODE_ELLIPSE;
235 | } else if (g_ascii_strcasecmp(paint_mode, "arrow") == 0) {
236 | config->paint_mode = SWAPPY_PAINT_MODE_ARROW;
237 | } else if (g_ascii_strcasecmp(paint_mode, "blur") == 0) {
238 | config->paint_mode = SWAPPY_PAINT_MODE_BLUR;
239 | } else {
240 | g_warning(
241 | "paint_mode is not a valid value: %s - see man page for details",
242 | paint_mode);
243 | }
244 | } else {
245 | g_info("paint_mode is missing in %s (%s)", file, error->message);
246 | g_error_free(error);
247 | error = NULL;
248 | }
249 |
250 | fill_shape = g_key_file_get_boolean(gkf, group, "fill_shape", &error);
251 |
252 | if (error == NULL) {
253 | config->fill_shape = fill_shape;
254 | } else {
255 | g_info("fill_shape is missing in %s (%s)", file, error->message);
256 | g_error_free(error);
257 | error = NULL;
258 | }
259 |
260 | auto_save = g_key_file_get_boolean(gkf, group, "auto_save", &error);
261 |
262 | if (error == NULL) {
263 | config->auto_save = auto_save;
264 | } else {
265 | g_info("auto_save is missing in %s (%s)", file, error->message);
266 | g_error_free(error);
267 | error = NULL;
268 | }
269 |
270 | custom_color = g_key_file_get_string(gkf, group, "custom_color", &error);
271 |
272 | if (error == NULL) {
273 | config->custom_color = custom_color;
274 | } else {
275 | g_info("custom_color is missing in %s (%s)", file, error->message);
276 | g_error_free(error);
277 | error = NULL;
278 | }
279 |
280 | transparent = g_key_file_get_boolean(gkf, group, "transparent", &error);
281 |
282 | if (error == NULL) {
283 | config->transparent = transparent;
284 | } else {
285 | g_info("transparent is missing in %s (%s)", file, error->message);
286 | g_error_free(error);
287 | error = NULL;
288 | }
289 |
290 | g_key_file_free(gkf);
291 | }
292 |
293 | static void load_default_config(struct swappy_config *config) {
294 | if (config == NULL) {
295 | return;
296 | }
297 |
298 | config->save_dir = get_default_save_dir();
299 | config->save_filename_format = g_strdup(CONFIG_SAVE_FILENAME_FORMAT_DEFAULT);
300 | config->line_size = CONFIG_LINE_SIZE_DEFAULT;
301 | config->text_font = g_strdup(CONFIG_TEXT_FONT_DEFAULT);
302 | config->text_size = CONFIG_TEXT_SIZE_DEFAULT;
303 | config->show_panel = CONFIG_SHOW_PANEL_DEFAULT;
304 | config->paint_mode = CONFIG_PAINT_MODE_DEFAULT;
305 | config->early_exit = CONFIG_EARLY_EXIT_DEFAULT;
306 | config->fill_shape = CONFIG_FILL_SHAPE_DEFAULT;
307 | config->auto_save = CONFIG_AUTO_SAVE_DEFAULT;
308 | config->custom_color = g_strdup(CONFIG_CUSTOM_COLOR_DEFAULT);
309 | config->transparent = CONFIG_TRANSPARENT_DEFAULT;
310 | config->transparency = CONFIG_TRANSPARENCY_DEFAULT;
311 | }
312 |
313 | void config_load(struct swappy_state *state) {
314 | struct swappy_config *config = g_new(struct swappy_config, 1);
315 |
316 | load_default_config(config);
317 |
318 | char *file = get_config_file();
319 | if (file) {
320 | load_config_from_file(config, file);
321 | } else {
322 | g_info("could not find swappy config file, using defaults");
323 | }
324 |
325 | config->config_file = file;
326 | state->config = config;
327 |
328 | print_config(state->config);
329 | }
330 |
331 | void config_free(struct swappy_state *state) {
332 | if (state->config) {
333 | g_free(state->config->config_file);
334 | g_free(state->config->save_dir);
335 | g_free(state->config->save_filename_format);
336 | g_free(state->config->text_font);
337 | g_free(state->config->custom_color);
338 | g_free(state->config);
339 | state->config = NULL;
340 | }
341 | }
342 |
--------------------------------------------------------------------------------
/src/file.c:
--------------------------------------------------------------------------------
1 | #define _POSIX_C_SOURCE 200112L
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #define BLOCK_SIZE 1024
13 |
14 | bool folder_exists(const char *path) {
15 | return g_file_test(path, G_FILE_TEST_IS_DIR);
16 | }
17 |
18 | bool file_exists(const char *path) {
19 | return g_file_test(path, G_FILE_TEST_EXISTS);
20 | }
21 |
22 | char *file_dump_stdin_into_a_temp_file() {
23 | char buf[BLOCK_SIZE];
24 | GError *error = NULL;
25 |
26 | if (isatty(STDIN_FILENO)) {
27 | g_warning("stdin is a tty");
28 | return NULL;
29 | }
30 |
31 | // Reopen stdin as binary mode
32 | FILE *input_file = g_freopen(NULL, "rb", stdin);
33 |
34 | if (!input_file) {
35 | g_warning("unable to reopen stdin in binary mode: %s", g_strerror(errno));
36 | return NULL;
37 | }
38 |
39 | const gchar *tempdir = g_get_tmp_dir();
40 | gchar filename[] = "swappy-stdin-XXXXXX.png";
41 | gchar *ret = g_build_filename(tempdir, filename, NULL);
42 | gint fd = g_mkstemp(ret);
43 |
44 | if (fd == -1) {
45 | g_warning("unable to dump stdin into temporary file");
46 | return NULL;
47 | }
48 |
49 | g_info("writing stdin content into filepath: %s", ret);
50 |
51 | size_t count = 1;
52 | while (count > 0) {
53 | count = fread(buf, 1, sizeof(buf), stdin);
54 | if (write(fd, &buf, count) == -1) {
55 | g_warning("error while writing stdin to temporary file: %s - %s", ret,
56 | g_strerror(errno));
57 | }
58 | }
59 |
60 | g_close(fd, &error);
61 |
62 | if (error) {
63 | g_warning("unable to close temporary file: %s", error->message);
64 | g_error_free(error);
65 | return NULL;
66 | }
67 |
68 | return ret;
69 | }
70 |
--------------------------------------------------------------------------------
/src/main.c:
--------------------------------------------------------------------------------
1 | #define _POSIX_C_SOURCE 200809L
2 |
3 | #include "application.h"
4 | #include "config.h"
5 |
6 | int main(int argc, char *argv[]) {
7 | struct swappy_state state = {0};
8 | int status;
9 |
10 | state.argc = argc;
11 | state.argv = argv;
12 | state.mode = SWAPPY_PAINT_MODE_BRUSH;
13 |
14 | if (!application_init(&state)) {
15 | g_critical("failed to initialize gtk application");
16 | exit(1);
17 | }
18 |
19 | status = application_run(&state);
20 |
21 | if (status == 0) {
22 | gtk_main();
23 | }
24 |
25 | application_finish(&state);
26 |
27 | return status;
28 | }
29 |
--------------------------------------------------------------------------------
/src/paint.c:
--------------------------------------------------------------------------------
1 | #include "paint.h"
2 |
3 | #include
4 | #include
5 |
6 | #include "gtk/gtk.h"
7 | #include "util.h"
8 |
9 | static void cursor_move_backward(struct swappy_paint_text *text) {
10 | if (text->cursor > 0) {
11 | text->cursor--;
12 | }
13 | }
14 |
15 | static void cursor_move_forward(struct swappy_paint_text *text) {
16 | if (text->cursor < g_utf8_strlen(text->text, -1)) {
17 | text->cursor++;
18 | }
19 | }
20 |
21 | void paint_free(gpointer data) {
22 | struct swappy_paint *paint = (struct swappy_paint *)data;
23 |
24 | if (paint == NULL) {
25 | return;
26 | }
27 |
28 | switch (paint->type) {
29 | case SWAPPY_PAINT_MODE_BLUR:
30 | if (paint->content.blur.surface) {
31 | cairo_surface_destroy(paint->content.blur.surface);
32 | }
33 | break;
34 | case SWAPPY_PAINT_MODE_BRUSH:
35 | g_list_free_full(paint->content.brush.points, g_free);
36 | break;
37 | case SWAPPY_PAINT_MODE_TEXT:
38 | g_free(paint->content.text.text);
39 | g_free(paint->content.text.font);
40 | break;
41 | default:
42 | break;
43 | }
44 | g_free(paint);
45 | }
46 |
47 | void paint_free_list(GList **list) {
48 | if (*list) {
49 | g_list_free_full(*list, paint_free);
50 | *list = NULL;
51 | }
52 | }
53 |
54 | void paint_free_all(struct swappy_state *state) {
55 | paint_free_list(&state->paints);
56 | paint_free_list(&state->redo_paints);
57 | paint_free(state->temp_paint);
58 | state->temp_paint = NULL;
59 | }
60 |
61 | void paint_add_temporary(struct swappy_state *state, double x, double y,
62 | enum swappy_paint_type type) {
63 | struct swappy_paint *paint = g_new(struct swappy_paint, 1);
64 | struct swappy_point *point;
65 |
66 | double r = state->settings.r;
67 | double g = state->settings.g;
68 | double b = state->settings.b;
69 | double a = state->settings.a;
70 | double w = state->settings.w;
71 | double t = state->settings.t;
72 |
73 | if (state->config->transparent) a *= (1 - state->settings.tr / 100.0);
74 |
75 | paint->type = type;
76 | paint->is_committed = false;
77 |
78 | g_debug("adding temporary paint at: %.2lfx%.2lf", x, y);
79 |
80 | if (state->temp_paint) {
81 | if (type == SWAPPY_PAINT_MODE_TEXT) {
82 | paint_commit_temporary(state);
83 | } else {
84 | paint_free(state->temp_paint);
85 | state->temp_paint = NULL;
86 | }
87 | }
88 |
89 | switch (type) {
90 | case SWAPPY_PAINT_MODE_BLUR:
91 | paint->can_draw = false;
92 |
93 | paint->content.blur.from.x = x;
94 | paint->content.blur.from.y = y;
95 | paint->content.blur.surface = NULL;
96 | break;
97 | case SWAPPY_PAINT_MODE_BRUSH:
98 | paint->can_draw = true;
99 |
100 | paint->content.brush.r = r;
101 | paint->content.brush.g = g;
102 | paint->content.brush.b = b;
103 | paint->content.brush.a = a;
104 | paint->content.brush.w = w;
105 |
106 | point = g_new(struct swappy_point, 1);
107 | point->x = x;
108 | point->y = y;
109 |
110 | paint->content.brush.points = g_list_prepend(NULL, point);
111 | break;
112 | case SWAPPY_PAINT_MODE_RECTANGLE:
113 | case SWAPPY_PAINT_MODE_ELLIPSE:
114 | case SWAPPY_PAINT_MODE_ARROW:
115 | paint->can_draw = false; // need `to` vector
116 |
117 | paint->content.shape.from.x = x;
118 | paint->content.shape.from.y = y;
119 | paint->content.shape.r = r;
120 | paint->content.shape.g = g;
121 | paint->content.shape.b = b;
122 | paint->content.shape.a = a;
123 | paint->content.shape.w = w;
124 | paint->content.shape.type = type;
125 | if (state->config->fill_shape)
126 | paint->content.shape.operation = SWAPPY_PAINT_SHAPE_OPERATION_FILL;
127 | else
128 | paint->content.shape.operation = SWAPPY_PAINT_SHAPE_OPERATION_STROKE;
129 | break;
130 | case SWAPPY_PAINT_MODE_TEXT:
131 | paint->can_draw = false;
132 |
133 | paint->content.text.from.x = x;
134 | paint->content.text.from.y = y;
135 | paint->content.text.r = r;
136 | paint->content.text.g = g;
137 | paint->content.text.b = b;
138 | paint->content.text.a = a;
139 | paint->content.text.s = t;
140 | paint->content.text.font = g_strdup(state->config->text_font);
141 | paint->content.text.cursor = 0;
142 | paint->content.text.mode = SWAPPY_TEXT_MODE_EDIT;
143 | paint->content.text.text = g_new(gchar, 1);
144 | paint->content.text.text[0] = '\0';
145 | break;
146 |
147 | default:
148 | g_info("unable to add temporary paint: %d", type);
149 | break;
150 | }
151 |
152 | state->temp_paint = paint;
153 | }
154 |
155 | void paint_update_temporary_shape(struct swappy_state *state, double x,
156 | double y, gboolean is_control_pressed) {
157 | struct swappy_paint *paint = state->temp_paint;
158 | struct swappy_point *point;
159 | GList *points;
160 |
161 | if (!paint) {
162 | return;
163 | }
164 |
165 | switch (paint->type) {
166 | case SWAPPY_PAINT_MODE_BLUR:
167 | paint->can_draw = true;
168 | paint->content.blur.to.x = x;
169 | paint->content.blur.to.y = y;
170 | break;
171 | case SWAPPY_PAINT_MODE_BRUSH:
172 | points = paint->content.brush.points;
173 | point = g_new(struct swappy_point, 1);
174 | point->x = x;
175 | point->y = y;
176 |
177 | paint->content.brush.points = g_list_prepend(points, point);
178 | break;
179 | case SWAPPY_PAINT_MODE_RECTANGLE:
180 | case SWAPPY_PAINT_MODE_ELLIPSE:
181 | paint->can_draw = true; // all set
182 |
183 | paint->content.shape.should_center_at_from = is_control_pressed;
184 | paint->content.shape.to.x = x;
185 | paint->content.shape.to.y = y;
186 | break;
187 | case SWAPPY_PAINT_MODE_ARROW:
188 | paint->can_draw = true; // all set
189 |
190 | paint->content.shape.to.x = x;
191 | paint->content.shape.to.y = y;
192 | break;
193 | default:
194 | g_info("unable to update temporary paint when type is: %d", paint->type);
195 | break;
196 | }
197 | }
198 |
199 | void paint_update_temporary_str(struct swappy_state *state, char *str) {
200 | struct swappy_paint *paint = state->temp_paint;
201 | struct swappy_paint_text *text;
202 | char *new_text;
203 | if (!paint || paint->type != SWAPPY_PAINT_MODE_TEXT) {
204 | g_warning("trying to update text but not in text mode");
205 | return;
206 | }
207 |
208 | text = &paint->content.text;
209 | new_text = string_insert_chars_at(text->text, str, text->cursor);
210 | g_free(text->text);
211 | text->text = new_text;
212 | text->cursor += g_utf8_strlen(str, -1);
213 | }
214 |
215 | void paint_update_temporary_text(struct swappy_state *state,
216 | GdkEventKey *event) {
217 | struct swappy_paint *paint = state->temp_paint;
218 | struct swappy_paint_text *text;
219 | char *new_text;
220 | char buffer[32];
221 | guint32 unicode;
222 |
223 | if (!paint || paint->type != SWAPPY_PAINT_MODE_TEXT) {
224 | g_warning("trying to update text but not in text mode");
225 | return;
226 | }
227 |
228 | text = &paint->content.text;
229 |
230 | switch (event->keyval) {
231 | case GDK_KEY_Escape:
232 | paint_commit_temporary(state);
233 | break;
234 | case GDK_KEY_BackSpace:
235 | if (g_utf8_strlen(text->text, -1) > 0) {
236 | new_text = string_remove_at(text->text, text->cursor - 1);
237 | g_free(text->text);
238 | text->text = new_text;
239 | cursor_move_backward(text);
240 | }
241 | break;
242 | case GDK_KEY_Delete:
243 | if (g_utf8_strlen(text->text, -1) > 0) {
244 | new_text = string_remove_at(text->text, text->cursor);
245 | g_free(text->text);
246 | text->text = new_text;
247 | }
248 | break;
249 | case GDK_KEY_Left:
250 | cursor_move_backward(text);
251 | break;
252 | case GDK_KEY_Right:
253 | cursor_move_forward(text);
254 | break;
255 | case GDK_KEY_V:
256 | cursor_move_forward(text);
257 | break;
258 | default:
259 | unicode = gdk_keyval_to_unicode(event->keyval);
260 | if (unicode != 0) {
261 | int ll = g_unichar_to_utf8(unicode, buffer);
262 | buffer[ll] = '\0';
263 | char *new_text =
264 | string_insert_chars_at(text->text, buffer, text->cursor);
265 | g_free(text->text);
266 | text->text = new_text;
267 | text->cursor++;
268 | }
269 | break;
270 | }
271 | }
272 |
273 | void paint_update_temporary_text_clip(struct swappy_state *state, gdouble x,
274 | gdouble y) {
275 | struct swappy_paint *paint = state->temp_paint;
276 |
277 | if (!paint) {
278 | return;
279 | }
280 |
281 | g_assert(paint->type == SWAPPY_PAINT_MODE_TEXT);
282 |
283 | paint->can_draw = true;
284 | paint->content.text.to.x = x;
285 | paint->content.text.to.y = y;
286 | gtk_im_context_focus_in(state->ui->im_context);
287 | }
288 |
289 | void paint_commit_temporary(struct swappy_state *state) {
290 | struct swappy_paint *paint = state->temp_paint;
291 |
292 | if (!paint) {
293 | return;
294 | }
295 |
296 | switch (paint->type) {
297 | case SWAPPY_PAINT_MODE_TEXT:
298 | if (g_utf8_strlen(paint->content.text.text, -1) == 0) {
299 | paint->can_draw = false;
300 | }
301 | paint->content.text.mode = SWAPPY_TEXT_MODE_DONE;
302 | break;
303 | default:
304 | break;
305 | }
306 |
307 | if (!paint->can_draw) {
308 | paint_free(paint);
309 | } else {
310 | paint->is_committed = true;
311 | state->paints = g_list_prepend(state->paints, paint);
312 | }
313 |
314 | gtk_im_context_focus_out(state->ui->im_context);
315 | // Set the temporary paint to NULL but keep the content in memory
316 | // because it's now part of the GList.
317 | state->temp_paint = NULL;
318 | }
319 |
--------------------------------------------------------------------------------
/src/pixbuf.c:
--------------------------------------------------------------------------------
1 | #include "pixbuf.h"
2 |
3 | #include
4 | #include
5 |
6 | GdkPixbuf *pixbuf_get_from_state(struct swappy_state *state) {
7 | guint width = cairo_image_surface_get_width(state->rendering_surface);
8 | guint height = cairo_image_surface_get_height(state->rendering_surface);
9 | GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(state->rendering_surface, 0,
10 | 0, width, height);
11 |
12 | return pixbuf;
13 | }
14 |
15 | static void write_file(GdkPixbuf *pixbuf, char *path) {
16 | GError *error = NULL;
17 | gdk_pixbuf_savev(pixbuf, path, "png", NULL, NULL, &error);
18 |
19 | if (error != NULL) {
20 | g_critical("unable to save drawing area to pixbuf: %s", error->message);
21 | g_error_free(error);
22 | }
23 | }
24 |
25 | void pixbuf_save_state_to_folder(GdkPixbuf *pixbuf, char *folder,
26 | char *filename_format) {
27 | time_t current_time = time(NULL);
28 | char *c_time_string;
29 | char filename[255];
30 | char path[MAX_PATH];
31 | size_t bytes_formated;
32 |
33 | c_time_string = ctime(¤t_time);
34 | c_time_string[strlen(c_time_string) - 1] = '\0';
35 | bytes_formated = strftime(filename, sizeof(filename), filename_format,
36 | localtime(¤t_time));
37 | if (!bytes_formated) {
38 | g_warning(
39 | "filename_format: %s overflows filename limit - file cannot be saved",
40 | filename_format);
41 | return;
42 | }
43 |
44 | g_snprintf(path, MAX_PATH, "%s/%s", folder, filename);
45 | g_info("saving surface to path: %s", path);
46 | write_file(pixbuf, path);
47 | }
48 |
49 | void pixbuf_save_to_stdout(GdkPixbuf *pixbuf) {
50 | GOutputStream *out;
51 | GError *error = NULL;
52 |
53 | out = g_unix_output_stream_new(STDOUT_FILENO, TRUE);
54 |
55 | gdk_pixbuf_save_to_stream(pixbuf, out, "png", NULL, &error, NULL);
56 |
57 | if (error != NULL) {
58 | g_warning("unable to save surface to stdout: %s", error->message);
59 | g_error_free(error);
60 | return;
61 | }
62 |
63 | g_object_unref(out);
64 | }
65 |
66 | GdkPixbuf *pixbuf_init_from_file(struct swappy_state *state) {
67 | GError *error = NULL;
68 | char *file =
69 | state->temp_file_str != NULL ? state->temp_file_str : state->file_str;
70 | GdkPixbuf *image = gdk_pixbuf_new_from_file(file, &error);
71 |
72 | if (error != NULL) {
73 | g_printerr("unable to load file: %s - reason: %s\n", file, error->message);
74 | g_error_free(error);
75 | return NULL;
76 | }
77 |
78 | state->original_image = image;
79 | return image;
80 | }
81 |
82 | void pixbuf_save_to_file(GdkPixbuf *pixbuf, char *file) {
83 | if (g_strcmp0(file, "-") == 0) {
84 | pixbuf_save_to_stdout(pixbuf);
85 | } else {
86 | write_file(pixbuf, file);
87 | }
88 | }
89 |
90 | void pixbuf_scale_surface_from_widget(struct swappy_state *state,
91 | GtkWidget *widget) {
92 | GtkAllocation *alloc = g_new(GtkAllocation, 1);
93 | GdkPixbuf *image = state->original_image;
94 | gtk_widget_get_allocation(widget, alloc);
95 |
96 | cairo_format_t format = CAIRO_FORMAT_ARGB32;
97 | gint image_width = gdk_pixbuf_get_width(image);
98 | gint image_height = gdk_pixbuf_get_height(image);
99 |
100 | cairo_surface_t *original_image_surface =
101 | cairo_image_surface_create(format, image_width, image_height);
102 |
103 | if (!original_image_surface) {
104 | g_error("unable to create cairo original surface from pixbuf");
105 | goto finish;
106 | } else {
107 | cairo_t *cr;
108 | cr = cairo_create(original_image_surface);
109 | gdk_cairo_set_source_pixbuf(cr, image, 0, 0);
110 | cairo_paint(cr);
111 | cairo_destroy(cr);
112 | }
113 |
114 | cairo_surface_t *rendering_surface =
115 | cairo_image_surface_create(format, image_width, image_height);
116 |
117 | if (!rendering_surface) {
118 | g_error("unable to create rendering surface");
119 | goto finish;
120 | }
121 |
122 | g_info("size of area to render: %ux%u", alloc->width, alloc->height);
123 |
124 | finish:
125 | if (state->original_image_surface) {
126 | cairo_surface_destroy(state->original_image_surface);
127 | state->original_image_surface = NULL;
128 | }
129 | state->original_image_surface = original_image_surface;
130 |
131 | if (state->rendering_surface) {
132 | cairo_surface_destroy(state->rendering_surface);
133 | state->rendering_surface = NULL;
134 | }
135 | state->rendering_surface = rendering_surface;
136 |
137 | g_free(alloc);
138 | }
139 |
140 | void pixbuf_free(struct swappy_state *state) {
141 | if (G_IS_OBJECT(state->original_image)) {
142 | g_object_unref(state->original_image);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/po/LINGUAS:
--------------------------------------------------------------------------------
1 | # Set of available languages.
2 | de
3 | fr
4 | en
5 | pt_BR
6 | tr
7 | zh_CN
8 |
--------------------------------------------------------------------------------
/src/po/POTFILES:
--------------------------------------------------------------------------------
1 | res/swappy.glade
2 |
--------------------------------------------------------------------------------
/src/po/de.po:
--------------------------------------------------------------------------------
1 | # German translations for swappy package.
2 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the swappy package.
4 | # Brodi , 2020.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: swappy\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n"
11 | "PO-Revision-Date: 2020-11-19 18:03+0300\n"
12 | "Last-Translator: Brodi \n"
13 | "Language-Team: none\n"
14 | "Language: de\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 |
20 | #: res/swappy.glade:461
21 | msgid "Line Width"
22 | msgstr "Linienstärke"
23 |
24 | #: res/swappy.glade:531
25 | msgid "Text Size"
26 | msgstr "Textgröße"
27 |
28 | #: res/swappy.glade:601
29 | msgid "Transparency"
30 | msgstr ""
31 |
32 | #: res/swappy.glade:667
33 | msgid "Fill shape"
34 | msgstr ""
35 |
36 | #: res/swappy.glade:672
37 | msgid "Toggle shape filling"
38 | msgstr ""
39 |
40 | #: res/swappy.glade:684
41 | msgid "Transparent"
42 | msgstr ""
43 |
44 | #: res/swappy.glade:689
45 | msgid "Toggle transparency"
46 | msgstr ""
47 |
48 | #: res/swappy.glade:763
49 | msgid "Toggle Paint Panel"
50 | msgstr "Farbtafel umschalten"
51 |
52 | #: res/swappy.glade:789
53 | msgid "Undo Last Paint"
54 | msgstr "Letzte Bemalung rückgängig machen"
55 |
56 | #: res/swappy.glade:808
57 | msgid "Redo Previous Paint"
58 | msgstr "Vorherige Bemalung wiederherstellen"
59 |
60 | #: res/swappy.glade:827
61 | msgid "Clear Paints"
62 | msgstr "Bemalung löschen"
63 |
64 | #: res/swappy.glade:855
65 | msgid "Copy Surface"
66 | msgstr "Fläche kopieren"
67 |
68 | #: res/swappy.glade:871
69 | msgid "Save Surface"
70 | msgstr "Fläche speichern"
71 |
--------------------------------------------------------------------------------
/src/po/en.po:
--------------------------------------------------------------------------------
1 | # English translations for swappy package.
2 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the swappy package.
4 | # Automatically generated, 2020.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: swappy\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n"
11 | "PO-Revision-Date: 2020-06-21 21:57-0400\n"
12 | "Last-Translator: Automatically generated\n"
13 | "Language-Team: none\n"
14 | "Language: en\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=ASCII\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 |
20 | #: res/swappy.glade:461
21 | msgid "Line Width"
22 | msgstr "Line Width"
23 |
24 | #: res/swappy.glade:531
25 | msgid "Text Size"
26 | msgstr "Text Size"
27 |
28 | #: res/swappy.glade:601
29 | msgid "Transparency"
30 | msgstr ""
31 |
32 | #: res/swappy.glade:667
33 | msgid "Fill shape"
34 | msgstr "Fill shape"
35 |
36 | #: res/swappy.glade:672
37 | msgid "Toggle shape filling"
38 | msgstr "Toggle shape filling"
39 |
40 | #: res/swappy.glade:684
41 | msgid "Transparent"
42 | msgstr ""
43 |
44 | #: res/swappy.glade:689
45 | msgid "Toggle transparency"
46 | msgstr ""
47 |
48 | #: res/swappy.glade:763
49 | msgid "Toggle Paint Panel"
50 | msgstr "Toggle Paint Panel"
51 |
52 | #: res/swappy.glade:789
53 | msgid "Undo Last Paint"
54 | msgstr "Undo Last Paint"
55 |
56 | #: res/swappy.glade:808
57 | msgid "Redo Previous Paint"
58 | msgstr "Redo Previous Paint"
59 |
60 | #: res/swappy.glade:827
61 | msgid "Clear Paints"
62 | msgstr "Clear Paints"
63 |
64 | #: res/swappy.glade:855
65 | msgid "Copy Surface"
66 | msgstr "Copy Surface"
67 |
68 | #: res/swappy.glade:871
69 | msgid "Save Surface"
70 | msgstr "Save Surface"
71 |
--------------------------------------------------------------------------------
/src/po/fr.po:
--------------------------------------------------------------------------------
1 | # French translations for swappy package.
2 | # Copyright (C) 2021 THE swappy'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the swappy package.
4 | # Jeremy Attali , 2021.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: swappy\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n"
11 | "PO-Revision-Date: 2021-02-20 21:00-0500\n"
12 | "Last-Translator: Jeremy Attali \n"
13 | "Language-Team: none\n"
14 | "Language: fr\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
19 |
20 | #: res/swappy.glade:461
21 | msgid "Line Width"
22 | msgstr "Epaisseur de ligne"
23 |
24 | #: res/swappy.glade:531
25 | msgid "Text Size"
26 | msgstr "Taille du texte"
27 |
28 | #: res/swappy.glade:601
29 | msgid "Transparency"
30 | msgstr ""
31 |
32 | #: res/swappy.glade:667
33 | msgid "Fill shape"
34 | msgstr "Remplir la forme"
35 |
36 | #: res/swappy.glade:672
37 | msgid "Toggle shape filling"
38 | msgstr "Activer/Désactiver le remplissage de forme"
39 |
40 | #: res/swappy.glade:684
41 | msgid "Transparent"
42 | msgstr ""
43 |
44 | #: res/swappy.glade:689
45 | msgid "Toggle transparency"
46 | msgstr ""
47 |
48 | #: res/swappy.glade:763
49 | msgid "Toggle Paint Panel"
50 | msgstr "Afficher/Cacher le panneau de peinture"
51 |
52 | #: res/swappy.glade:789
53 | msgid "Undo Last Paint"
54 | msgstr "Annuler la dernière peinture"
55 |
56 | #: res/swappy.glade:808
57 | msgid "Redo Previous Paint"
58 | msgstr "Rétablir la dernière peinture"
59 |
60 | #: res/swappy.glade:827
61 | msgid "Clear Paints"
62 | msgstr "Supprimer les peintures"
63 |
64 | #: res/swappy.glade:855
65 | msgid "Copy Surface"
66 | msgstr "Copier la surface"
67 |
68 | #: res/swappy.glade:871
69 | msgid "Save Surface"
70 | msgstr "Sauvegarder la surface"
71 |
--------------------------------------------------------------------------------
/src/po/meson.build:
--------------------------------------------------------------------------------
1 | i18n = import('i18n')
2 |
3 | # define GETTEXT_PACKAGE
4 | add_project_arguments('-DGETTEXT_PACKAGE="swappy"', language:'c')
5 |
6 | i18n.gettext(meson.project_name(),
7 | args: '--directory=' + meson.source_root()
8 | )
9 |
10 | # Translate and install our .desktop file
11 | i18n.merge_file(
12 | input: meson.project_name() + '.desktop.in',
13 | output: meson.project_name() + '.desktop',
14 | po_dir: meson.current_source_dir(),
15 | type: 'desktop',
16 | install: true,
17 | install_dir: join_paths(get_option('datadir'), 'applications')
18 | )
19 |
--------------------------------------------------------------------------------
/src/po/pt_BR.po:
--------------------------------------------------------------------------------
1 | # Brazilian Portuguese translation for swappy
2 | # Copyright (C) 2020 Jeremy Attali
3 | # This file is distributed under the MIT License
4 | # Gustavo Costa , 2021.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: swappy\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n"
11 | "PO-Revision-Date: 2021-02-14 20:38-0300\n"
12 | "Last-Translator: Gustavo Costa \n"
13 | "Language-Team: \n"
14 | "Language: pt_BR\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "X-Generator: Poedit 2.4.2\n"
19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
20 |
21 | #: res/swappy.glade:461
22 | msgid "Line Width"
23 | msgstr "Espessura da linha"
24 |
25 | #: res/swappy.glade:531
26 | msgid "Text Size"
27 | msgstr "Tamanho do texto"
28 |
29 | #: res/swappy.glade:601
30 | msgid "Transparency"
31 | msgstr ""
32 |
33 | #: res/swappy.glade:667
34 | msgid "Fill shape"
35 | msgstr ""
36 |
37 | #: res/swappy.glade:672
38 | msgid "Toggle shape filling"
39 | msgstr ""
40 |
41 | #: res/swappy.glade:684
42 | msgid "Transparent"
43 | msgstr ""
44 |
45 | #: res/swappy.glade:689
46 | msgid "Toggle transparency"
47 | msgstr ""
48 |
49 | #: res/swappy.glade:763
50 | msgid "Toggle Paint Panel"
51 | msgstr "Alternar painel de pintura"
52 |
53 | #: res/swappy.glade:789
54 | msgid "Undo Last Paint"
55 | msgstr "Desfazer última pintura"
56 |
57 | #: res/swappy.glade:808
58 | msgid "Redo Previous Paint"
59 | msgstr "Refazer pintura anterior"
60 |
61 | #: res/swappy.glade:827
62 | msgid "Clear Paints"
63 | msgstr "Limpar pinturas"
64 |
65 | #: res/swappy.glade:855
66 | msgid "Copy Surface"
67 | msgstr "Copiar superfície"
68 |
69 | #: res/swappy.glade:871
70 | msgid "Save Surface"
71 | msgstr "Salvar superfície"
72 |
--------------------------------------------------------------------------------
/src/po/swappy.desktop.in:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Swappy
3 | GenericName=Annotation Tool
4 | GenericName[de]=Anmerkungswerkzeug
5 | GenericName[fr]=Outil d'annotation
6 | GenericName[pt_BR]=Ferramenta de Anotação
7 | GenericName[tr]=Açıklama Aracı
8 | Comment=A Wayland native snapshot editing tool
9 | Comment[de]=Ein natives Wayland Bildschirmfoto-Bearbeitungswerkzeug
10 | Comment[fr]=Un outil d'édition de capture d'écran avec support natif pour Wayland
11 | Comment[pt_BR]=Uma ferramenta de edição de snapshot nativa do Wayland
12 | Comment[tr]=Wayland için anlık görüntü düzenleme aracı
13 | TryExec=swappy
14 | Exec=swappy -f %f
15 | Terminal=false
16 | NoDisplay=true
17 | Type=Application
18 | Keywords=wayland;snapshot;annotation;editing;
19 | Icon=swappy
20 | Categories=Utility;Graphics;
21 | StartupNotify=true
22 | MimeType=image/png;image/jpeg;
23 |
--------------------------------------------------------------------------------
/src/po/swappy.pot:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the swappy package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: swappy\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2024-11-06 23:00+0100\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=CHARSET\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 |
20 | #: res/swappy.glade:461
21 | msgid "Line Width"
22 | msgstr ""
23 |
24 | #: res/swappy.glade:531
25 | msgid "Text Size"
26 | msgstr ""
27 |
28 | #: res/swappy.glade:601
29 | msgid "Transparency"
30 | msgstr ""
31 |
32 | #: res/swappy.glade:667
33 | msgid "Fill shape"
34 | msgstr ""
35 |
36 | #: res/swappy.glade:672
37 | msgid "Toggle shape filling"
38 | msgstr ""
39 |
40 | #: res/swappy.glade:684
41 | msgid "Transparent"
42 | msgstr ""
43 |
44 | #: res/swappy.glade:689
45 | msgid "Toggle transparency"
46 | msgstr ""
47 |
48 | #: res/swappy.glade:763
49 | msgid "Toggle Paint Panel"
50 | msgstr ""
51 |
52 | #: res/swappy.glade:789
53 | msgid "Undo Last Paint"
54 | msgstr ""
55 |
56 | #: res/swappy.glade:808
57 | msgid "Redo Previous Paint"
58 | msgstr ""
59 |
60 | #: res/swappy.glade:827
61 | msgid "Clear Paints"
62 | msgstr ""
63 |
64 | #: res/swappy.glade:855
65 | msgid "Copy Surface"
66 | msgstr ""
67 |
68 | #: res/swappy.glade:871
69 | msgid "Save Surface"
70 | msgstr ""
71 |
--------------------------------------------------------------------------------
/src/po/tr.po:
--------------------------------------------------------------------------------
1 | # Turkish translations for swappy package.
2 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the swappy package.
4 | # Oğuz Ersen , 2020-2022.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: swappy\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n"
11 | "PO-Revision-Date: 2022-11-25 10:36+0300\n"
12 | "Last-Translator: Oğuz Ersen \n"
13 | "Language-Team: Turkish \n"
14 | "Language: tr\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19 |
20 | #: res/swappy.glade:461
21 | msgid "Line Width"
22 | msgstr "Çizgi Genişliği"
23 |
24 | #: res/swappy.glade:531
25 | msgid "Text Size"
26 | msgstr "Metin Boyutu"
27 |
28 | #: res/swappy.glade:601
29 | msgid "Transparency"
30 | msgstr ""
31 |
32 | #: res/swappy.glade:667
33 | msgid "Fill shape"
34 | msgstr "Şekli Doldur"
35 |
36 | #: res/swappy.glade:672
37 | msgid "Toggle shape filling"
38 | msgstr "Şekil Doldurmayı Aç/Kapat"
39 |
40 | #: res/swappy.glade:684
41 | msgid "Transparent"
42 | msgstr ""
43 |
44 | #: res/swappy.glade:689
45 | msgid "Toggle transparency"
46 | msgstr ""
47 |
48 | #: res/swappy.glade:763
49 | msgid "Toggle Paint Panel"
50 | msgstr "Boyama Panelini Aç/Kapat"
51 |
52 | #: res/swappy.glade:789
53 | msgid "Undo Last Paint"
54 | msgstr "Son Boyamayı Geri Al"
55 |
56 | #: res/swappy.glade:808
57 | msgid "Redo Previous Paint"
58 | msgstr "Önceki Boyamayı Tekrarla"
59 |
60 | #: res/swappy.glade:827
61 | msgid "Clear Paints"
62 | msgstr "Boyamaları Temizle"
63 |
64 | #: res/swappy.glade:855
65 | msgid "Copy Surface"
66 | msgstr "Yüzeyi Kopyala"
67 |
68 | #: res/swappy.glade:871
69 | msgid "Save Surface"
70 | msgstr "Yüzeyi Kaydet"
71 |
--------------------------------------------------------------------------------
/src/po/zh_CN.po:
--------------------------------------------------------------------------------
1 | # English translations for swappy package.
2 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the swappy package.
4 | # Automatically generated, 2020.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: swappy\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n"
11 | "PO-Revision-Date: 2020-06-21 21:57-0400\n"
12 | "Last-Translator: Automatically generated\n"
13 | "Language-Team: none\n"
14 | "Language: zh_CN\n"
15 | "MIME-Version: 1.0\n"
16 | "Content-Type: text/plain; charset=UTF-8\n"
17 | "Content-Transfer-Encoding: 8bit\n"
18 | "Plural-Forms: nplurals=1; plural=0;\n"
19 |
20 | #: res/swappy.glade:461
21 | msgid "Line Width"
22 | msgstr "行宽"
23 |
24 | #: res/swappy.glade:531
25 | msgid "Text Size"
26 | msgstr "文本大小"
27 |
28 | #: res/swappy.glade:601
29 | msgid "Transparency"
30 | msgstr ""
31 |
32 | #: res/swappy.glade:667
33 | msgid "Fill shape"
34 | msgstr "填充"
35 |
36 | #: res/swappy.glade:672
37 | msgid "Toggle shape filling"
38 | msgstr "切换填充状态"
39 |
40 | #: res/swappy.glade:684
41 | msgid "Transparent"
42 | msgstr ""
43 |
44 | #: res/swappy.glade:689
45 | msgid "Toggle transparency"
46 | msgstr ""
47 |
48 | #: res/swappy.glade:763
49 | msgid "Toggle Paint Panel"
50 | msgstr "切换绘图板状态"
51 |
52 | #: res/swappy.glade:789
53 | msgid "Undo Last Paint"
54 | msgstr "撤销"
55 |
56 | #: res/swappy.glade:808
57 | msgid "Redo Previous Paint"
58 | msgstr "恢复"
59 |
60 | #: res/swappy.glade:827
61 | msgid "Clear Paints"
62 | msgstr "清除绘图"
63 |
64 | #: res/swappy.glade:855
65 | msgid "Copy Surface"
66 | msgstr "复制"
67 |
68 | #: res/swappy.glade:871
69 | msgid "Save Surface"
70 | msgstr "保存"
71 |
--------------------------------------------------------------------------------
/src/render.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | #include "algebra.h"
7 | #include "swappy.h"
8 | #include "util.h"
9 |
10 | #define pango_layout_t PangoLayout
11 | #define pango_font_description_t PangoFontDescription
12 | #define pango_rectangle_t PangoRectangle
13 |
14 | /*
15 | * This code was largely taken from Kristian Høgsberg and Chris Wilson from:
16 | * https://www.cairographics.org/cookbook/blur.c/
17 | */
18 | static cairo_surface_t *blur_surface(cairo_surface_t *surface, double x,
19 | double y, double width, double height) {
20 | cairo_surface_t *dest_surface, *tmp_surface, *final = NULL;
21 | cairo_t *cr;
22 | int src_width, src_height;
23 | int src_stride, dst_stride;
24 | guint u, v, w, z;
25 | uint8_t *dst, *tmp;
26 | uint32_t *s, *d, p;
27 | int i, j, k;
28 | const int radius = 4;
29 | const double sigma = 3.1;
30 | struct gaussian_kernel *gaussian = gaussian_kernel(radius, sigma);
31 | const int size = gaussian->size;
32 | const int half = (int)radius * 2;
33 | gdouble scale_x, scale_y;
34 | guint sum, pass, nb_passes;
35 |
36 | sum = (guint)gaussian->sum;
37 |
38 | if (cairo_surface_status(surface)) {
39 | return NULL;
40 | }
41 |
42 | cairo_surface_get_device_scale(surface, &scale_x, &scale_y);
43 |
44 | cairo_format_t src_format = cairo_image_surface_get_format(surface);
45 | switch (src_format) {
46 | case CAIRO_FORMAT_A1:
47 | case CAIRO_FORMAT_A8:
48 | default:
49 | g_warning("source surface format: %d is not supported", src_format);
50 | return NULL;
51 | case CAIRO_FORMAT_RGB24:
52 | case CAIRO_FORMAT_ARGB32:
53 | break;
54 | }
55 |
56 | src_stride = cairo_image_surface_get_stride(surface);
57 | src_width = cairo_image_surface_get_width(surface);
58 | src_height = cairo_image_surface_get_height(surface);
59 |
60 | g_assert(src_height >= height);
61 | g_assert(src_width >= width);
62 |
63 | dest_surface = cairo_image_surface_create(src_format, src_width, src_height);
64 | tmp_surface = cairo_image_surface_create(src_format, src_width, src_height);
65 |
66 | cairo_surface_set_device_scale(dest_surface, scale_x, scale_y);
67 | cairo_surface_set_device_scale(tmp_surface, scale_x, scale_y);
68 |
69 | if (cairo_surface_status(dest_surface) || cairo_surface_status(tmp_surface)) {
70 | goto cleanup;
71 | }
72 |
73 | cr = cairo_create(tmp_surface);
74 | cairo_set_source_surface(cr, surface, 0, 0);
75 | cairo_paint(cr);
76 | cairo_destroy(cr);
77 |
78 | cr = cairo_create(dest_surface);
79 | cairo_set_source_surface(cr, surface, 0, 0);
80 | cairo_paint(cr);
81 | cairo_destroy(cr);
82 |
83 | dst = cairo_image_surface_get_data(dest_surface);
84 | tmp = cairo_image_surface_get_data(tmp_surface);
85 | dst_stride = cairo_image_surface_get_stride(dest_surface);
86 |
87 | nb_passes = (guint)sqrt(scale_x * scale_y) + 1;
88 |
89 | int start_x = CLAMP(x * scale_x, 0, src_width);
90 | int start_y = CLAMP(y * scale_y, 0, src_height);
91 |
92 | int end_x = CLAMP((x + width) * scale_x, 0, src_width);
93 | int end_y = CLAMP((y + height) * scale_y, 0, src_height);
94 |
95 | for (pass = 0; pass < nb_passes; pass++) {
96 | /* Horizontally blur from dst -> tmp */
97 | for (i = start_y; i < end_y; i++) {
98 | s = (uint32_t *)(dst + i * src_stride);
99 | d = (uint32_t *)(tmp + i * dst_stride);
100 | for (j = start_x; j < end_x; j++) {
101 | u = v = w = z = 0;
102 | for (k = 0; k < size; k++) {
103 | gdouble multiplier = gaussian->kernel[k];
104 |
105 | if (j - half + k < 0 || j - half + k >= src_width) {
106 | continue;
107 | }
108 |
109 | p = s[j - half + k];
110 |
111 | u += ((p >> 24) & 0xff) * multiplier;
112 | v += ((p >> 16) & 0xff) * multiplier;
113 | w += ((p >> 8) & 0xff) * multiplier;
114 | z += ((p >> 0) & 0xff) * multiplier;
115 | }
116 |
117 | d[j] = (u / sum << 24) | (v / sum << 16) | (w / sum << 8) | z / sum;
118 | }
119 | }
120 |
121 | /* Then vertically blur from tmp -> dst */
122 | for (i = start_y; i < end_y; i++) {
123 | d = (uint32_t *)(dst + i * dst_stride);
124 | for (j = start_x; j < end_x; j++) {
125 | u = v = w = z = 0;
126 | for (k = 0; k < size; k++) {
127 | gdouble multiplier = gaussian->kernel[k];
128 |
129 | if (i - half + k < 0 || i - half + k >= src_height) {
130 | continue;
131 | }
132 |
133 | s = (uint32_t *)(tmp + (i - half + k) * dst_stride);
134 | p = s[j];
135 |
136 | u += ((p >> 24) & 0xff) * multiplier;
137 | v += ((p >> 16) & 0xff) * multiplier;
138 | w += ((p >> 8) & 0xff) * multiplier;
139 | z += ((p >> 0) & 0xff) * multiplier;
140 | }
141 |
142 | d[j] = (u / sum << 24) | (v / sum << 16) | (w / sum << 8) | z / sum;
143 | }
144 | }
145 | }
146 |
147 | // Mark destination surface as dirty since it was altered with custom data.
148 | cairo_surface_mark_dirty(dest_surface);
149 |
150 | final = cairo_image_surface_create(src_format, (int)(width * scale_x),
151 | (int)(height * scale_y));
152 |
153 | if (cairo_surface_status(final)) {
154 | goto cleanup;
155 | }
156 |
157 | cairo_surface_set_device_scale(final, scale_x, scale_y);
158 | cr = cairo_create(final);
159 | cairo_set_source_surface(cr, dest_surface, -x, -y);
160 | cairo_paint(cr);
161 | cairo_destroy(cr);
162 |
163 | cleanup:
164 | cairo_surface_destroy(dest_surface);
165 | cairo_surface_destroy(tmp_surface);
166 | gaussian_kernel_free(gaussian);
167 | return final;
168 | }
169 |
170 | static void convert_pango_rectangle_to_swappy_box(pango_rectangle_t rectangle,
171 | struct swappy_box *box) {
172 | if (!box) {
173 | return;
174 | }
175 |
176 | box->x = pango_units_to_double(rectangle.x);
177 | box->y = pango_units_to_double(rectangle.y);
178 | box->width = pango_units_to_double(rectangle.width);
179 | box->height = pango_units_to_double(rectangle.height);
180 | }
181 |
182 | static void render_text(cairo_t *cr, struct swappy_paint_text text,
183 | struct swappy_state *state) {
184 | char pango_font[255];
185 | double x = fmin(text.from.x, text.to.x);
186 | double y = fmin(text.from.y, text.to.y);
187 | double w = fabs(text.from.x - text.to.x);
188 | double h = fabs(text.from.y - text.to.y);
189 |
190 | cairo_surface_t *surface =
191 | cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
192 | cairo_t *crt = cairo_create(surface);
193 |
194 | pango_layout_t *layout = pango_cairo_create_layout(crt);
195 | pango_layout_set_text(layout, text.text, -1);
196 | g_snprintf(pango_font, 255, "%s %d", text.font, (int)text.s);
197 | pango_font_description_t *desc =
198 | pango_font_description_from_string(pango_font);
199 | pango_layout_set_width(layout, pango_units_from_double(w));
200 | pango_layout_set_font_description(layout, desc);
201 | pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
202 | pango_font_description_free(desc);
203 |
204 | if (text.mode == SWAPPY_TEXT_MODE_EDIT) {
205 | pango_rectangle_t strong_pos;
206 | struct swappy_box cursor_box;
207 | cairo_set_source_rgba(cr, 0.5, 0.5, 0.5, 0.3);
208 | cairo_set_line_width(cr, 5);
209 | cairo_rectangle(cr, x, y, w, h);
210 | cairo_stroke(cr);
211 | glong bytes_til_cursor = string_get_nb_bytes_until(text.text, text.cursor);
212 | pango_layout_get_cursor_pos(layout, bytes_til_cursor, &strong_pos, NULL);
213 | convert_pango_rectangle_to_swappy_box(strong_pos, &cursor_box);
214 | cairo_move_to(crt, cursor_box.x, cursor_box.y);
215 | cairo_set_source_rgba(crt, 0.3, 0.3, 0.3, 1);
216 | cairo_line_to(crt, cursor_box.x, cursor_box.y + cursor_box.height);
217 | cairo_stroke(crt);
218 | GdkRectangle area = {x + cursor_box.x, y + cursor_box.y + cursor_box.height,
219 | 0, 0};
220 | gtk_im_context_set_cursor_location(state->ui->im_context, &area);
221 | }
222 |
223 | cairo_rectangle(crt, 0, 0, w, h);
224 | cairo_set_source_rgba(crt, text.r, text.g, text.b, text.a);
225 | cairo_move_to(crt, 0, 0);
226 | pango_cairo_show_layout(crt, layout);
227 |
228 | cairo_set_source_surface(cr, surface, x, y);
229 | cairo_paint(cr);
230 |
231 | cairo_destroy(crt);
232 | cairo_surface_destroy(surface);
233 | g_object_unref(layout);
234 | }
235 |
236 | static void render_shape_arrow(cairo_t *cr, struct swappy_paint_shape shape) {
237 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a);
238 | cairo_set_line_width(cr, shape.w);
239 |
240 | double ftx = shape.to.x - shape.from.x;
241 | double fty = shape.to.y - shape.from.y;
242 | double ftn = sqrt(ftx * ftx + fty * fty);
243 |
244 | double r = 20;
245 | double scaling_factor = shape.w / 4;
246 |
247 | double alpha = G_PI / 6;
248 | double ta = 5 * alpha;
249 | double tb = 7 * alpha;
250 | double xa = r * cos(ta);
251 | double ya = r * sin(ta);
252 | double xb = r * cos(tb);
253 | double yb = r * sin(tb);
254 | double xc = ftn - fabs(xa) * scaling_factor;
255 |
256 | if (xc < DBL_EPSILON) {
257 | xc = 0;
258 | }
259 |
260 | if (ftn < DBL_EPSILON) {
261 | return;
262 | }
263 |
264 | double theta = copysign(1.0, fty) * acos(ftx / ftn);
265 |
266 | // Draw line
267 | cairo_save(cr);
268 | cairo_translate(cr, shape.from.x, shape.from.y);
269 | cairo_rotate(cr, theta);
270 | cairo_move_to(cr, 0, 0);
271 | cairo_line_to(cr, xc, 0);
272 | cairo_stroke(cr);
273 | cairo_restore(cr);
274 |
275 | // Draw arrow
276 | cairo_save(cr);
277 | cairo_translate(cr, shape.to.x, shape.to.y);
278 | cairo_rotate(cr, theta);
279 | cairo_scale(cr, scaling_factor, scaling_factor);
280 | cairo_move_to(cr, 0, 0);
281 | cairo_line_to(cr, xa, ya);
282 | cairo_line_to(cr, xb, yb);
283 | cairo_line_to(cr, 0, 0);
284 | cairo_fill(cr);
285 | cairo_restore(cr);
286 | }
287 |
288 | static void render_shape_ellipse(cairo_t *cr, struct swappy_paint_shape shape) {
289 | double x = fabs(shape.from.x - shape.to.x);
290 | double y = fabs(shape.from.y - shape.to.y);
291 |
292 | double n = sqrt(x * x + y * y);
293 |
294 | double xc, yc, r;
295 |
296 | if (shape.should_center_at_from) {
297 | xc = shape.from.x;
298 | yc = shape.from.y;
299 |
300 | r = n;
301 | } else {
302 | xc = shape.from.x + ((shape.to.x - shape.from.x) / 2);
303 | yc = shape.from.y + ((shape.to.y - shape.from.y) / 2);
304 |
305 | r = n / 2;
306 | }
307 |
308 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a);
309 | cairo_set_line_width(cr, shape.w);
310 |
311 | cairo_matrix_t save_matrix;
312 | cairo_get_matrix(cr, &save_matrix);
313 | cairo_translate(cr, xc, yc);
314 | cairo_scale(cr, x / n, y / n);
315 | cairo_arc(cr, 0, 0, r, 0, 2 * G_PI);
316 | cairo_set_matrix(cr, &save_matrix);
317 |
318 | switch (shape.operation) {
319 | case SWAPPY_PAINT_SHAPE_OPERATION_STROKE:
320 | cairo_stroke(cr);
321 | break;
322 | case SWAPPY_PAINT_SHAPE_OPERATION_FILL:
323 | cairo_fill(cr);
324 | break;
325 | default:
326 | cairo_stroke(cr);
327 | break;
328 | }
329 |
330 | cairo_close_path(cr);
331 | }
332 |
333 | static void render_shape_rectangle(cairo_t *cr,
334 | struct swappy_paint_shape shape) {
335 | double x, y, w, h;
336 |
337 | if (shape.should_center_at_from) {
338 | x = shape.from.x - fabs(shape.from.x - shape.to.x);
339 | y = shape.from.y - fabs(shape.from.y - shape.to.y);
340 | w = fabs(shape.from.x - shape.to.x) * 2;
341 | h = fabs(shape.from.y - shape.to.y) * 2;
342 | } else {
343 | x = fmin(shape.from.x, shape.to.x);
344 | y = fmin(shape.from.y, shape.to.y);
345 | w = fabs(shape.from.x - shape.to.x);
346 | h = fabs(shape.from.y - shape.to.y);
347 | }
348 |
349 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a);
350 | cairo_set_line_width(cr, shape.w);
351 |
352 | cairo_rectangle(cr, x, y, w, h);
353 | cairo_close_path(cr);
354 |
355 | switch (shape.operation) {
356 | case SWAPPY_PAINT_SHAPE_OPERATION_STROKE:
357 | cairo_stroke(cr);
358 | break;
359 | case SWAPPY_PAINT_SHAPE_OPERATION_FILL:
360 | cairo_fill(cr);
361 | break;
362 | default:
363 | cairo_stroke(cr);
364 | break;
365 | }
366 | }
367 |
368 | static void render_shape(cairo_t *cr, struct swappy_paint_shape shape) {
369 | cairo_save(cr);
370 | switch (shape.type) {
371 | case SWAPPY_PAINT_MODE_RECTANGLE:
372 | render_shape_rectangle(cr, shape);
373 | break;
374 | case SWAPPY_PAINT_MODE_ELLIPSE:
375 | render_shape_ellipse(cr, shape);
376 | break;
377 | case SWAPPY_PAINT_MODE_ARROW:
378 | render_shape_arrow(cr, shape);
379 | break;
380 | default:
381 | break;
382 | }
383 | cairo_restore(cr);
384 | }
385 |
386 | static void clear_surface(cairo_t *cr) {
387 | cairo_save(cr);
388 | cairo_set_source_rgba(cr, 0, 0, 0, 0);
389 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
390 | cairo_paint(cr);
391 | cairo_restore(cr);
392 | }
393 |
394 | static void render_blur(cairo_t *cr, struct swappy_paint *paint) {
395 | struct swappy_paint_blur blur = paint->content.blur;
396 |
397 | cairo_surface_t *target = cairo_get_target(cr);
398 |
399 | double x = MIN(blur.from.x, blur.to.x);
400 | double y = MIN(blur.from.y, blur.to.y);
401 | double w = ABS(blur.from.x - blur.to.x);
402 | double h = ABS(blur.from.y - blur.to.y);
403 |
404 | cairo_save(cr);
405 |
406 | if (paint->is_committed) {
407 | // Surface has already been blurred, reuse it in future passes
408 | if (blur.surface) {
409 | cairo_surface_t *surface = blur.surface;
410 | if (surface && cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) {
411 | cairo_set_source_surface(cr, surface, x, y);
412 | cairo_paint(cr);
413 | }
414 | } else {
415 | // Blur surface and reuse it in future passes
416 | g_info(
417 | "blurring surface on following image coordinates: %.2lf,%.2lf size: "
418 | "%.2lfx%.2lf",
419 | x, y, w, h);
420 | cairo_surface_t *blurred = blur_surface(target, x, y, w, h);
421 |
422 | if (blurred && cairo_surface_status(blurred) == CAIRO_STATUS_SUCCESS) {
423 | cairo_set_source_surface(cr, blurred, x, y);
424 | cairo_paint(cr);
425 | paint->content.blur.surface = blurred;
426 | }
427 | }
428 | } else {
429 | // Blur not committed yet, draw bounding rectangle
430 | struct swappy_paint_shape rect = {
431 | .r = 0,
432 | .g = 0.5,
433 | .b = 1,
434 | .a = 0.5,
435 | .w = 5,
436 | .from = blur.from,
437 | .to = blur.to,
438 | .type = SWAPPY_PAINT_MODE_RECTANGLE,
439 | .operation = SWAPPY_PAINT_SHAPE_OPERATION_FILL,
440 | };
441 | render_shape_rectangle(cr, rect);
442 | }
443 |
444 | cairo_restore(cr);
445 | }
446 |
447 | static void render_brush(cairo_t *cr, struct swappy_paint_brush brush) {
448 | cairo_set_source_rgba(cr, brush.r, brush.g, brush.b, brush.a);
449 | cairo_set_line_width(cr, brush.w);
450 | cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL);
451 |
452 | guint l = g_list_length(brush.points);
453 |
454 | if (l == 1) {
455 | struct swappy_point *point = g_list_nth_data(brush.points, 0);
456 | cairo_rectangle(cr, point->x, point->y, brush.w, brush.w);
457 | cairo_fill(cr);
458 | } else {
459 | for (GList *elem = brush.points; elem; elem = elem->next) {
460 | struct swappy_point *point = elem->data;
461 | cairo_line_to(cr, point->x, point->y);
462 | }
463 | cairo_stroke(cr);
464 | }
465 | }
466 |
467 | static void render_image(cairo_t *cr, struct swappy_state *state) {
468 | cairo_surface_t *surface = state->original_image_surface;
469 |
470 | cairo_save(cr);
471 |
472 | if (surface && !cairo_surface_status(surface)) {
473 | cairo_set_source_surface(cr, surface, 0, 0);
474 | cairo_paint(cr);
475 | }
476 |
477 | cairo_restore(cr);
478 | }
479 |
480 | static void render_paint(cairo_t *cr, struct swappy_paint *paint,
481 | struct swappy_state *state) {
482 | if (!paint->can_draw) {
483 | return;
484 | }
485 | switch (paint->type) {
486 | case SWAPPY_PAINT_MODE_BLUR:
487 | render_blur(cr, paint);
488 | break;
489 | case SWAPPY_PAINT_MODE_BRUSH:
490 | render_brush(cr, paint->content.brush);
491 | break;
492 | case SWAPPY_PAINT_MODE_RECTANGLE:
493 | case SWAPPY_PAINT_MODE_ELLIPSE:
494 | case SWAPPY_PAINT_MODE_ARROW:
495 | render_shape(cr, paint->content.shape);
496 | break;
497 | case SWAPPY_PAINT_MODE_TEXT:
498 | render_text(cr, paint->content.text, state);
499 | break;
500 | default:
501 | g_info("unable to render paint with type: %d", paint->type);
502 | break;
503 | }
504 | }
505 |
506 | static void render_paints(cairo_t *cr, struct swappy_state *state) {
507 | for (GList *elem = g_list_last(state->paints); elem; elem = elem->prev) {
508 | struct swappy_paint *paint = elem->data;
509 | render_paint(cr, paint, state);
510 | }
511 |
512 | if (state->temp_paint) {
513 | render_paint(cr, state->temp_paint, state);
514 | }
515 | }
516 |
517 | void render_state(struct swappy_state *state) {
518 | cairo_surface_t *surface = state->rendering_surface;
519 | cairo_t *cr = cairo_create(surface);
520 |
521 | clear_surface(cr);
522 | render_image(cr, state);
523 | render_paints(cr, state);
524 |
525 | cairo_destroy(cr);
526 |
527 | // Drawing is finished, notify the GtkDrawingArea it needs to be redrawn.
528 | gtk_widget_queue_draw(state->ui->area);
529 | }
530 |
--------------------------------------------------------------------------------
/src/util.c:
--------------------------------------------------------------------------------
1 | #include "util.h"
2 |
3 | #include
4 | #include
5 |
6 | gchar *string_remove_at(gchar *str, glong pos) {
7 | glong str_len = strlen(str);
8 | glong ustr_len = g_utf8_strlen(str, -1);
9 | gchar *new_str = g_new0(gchar, MAX(str_len, 1));
10 | gchar *buffer_source = str;
11 | gchar *buffer_copy = new_str;
12 | glong i = 0;
13 | gint bytes;
14 | gunichar c;
15 |
16 | if (pos <= ustr_len && g_utf8_validate(str, -1, NULL)) {
17 | while (*buffer_source != '\0') {
18 | c = g_utf8_get_char(buffer_source);
19 | buffer_source = g_utf8_next_char(buffer_source);
20 | if (i != pos) {
21 | bytes = g_unichar_to_utf8(c, buffer_copy);
22 | buffer_copy += bytes;
23 | }
24 | i++;
25 | }
26 | }
27 |
28 | return new_str;
29 | }
30 |
31 | gchar *string_insert_chars_at(gchar *str, gchar *chars, glong pos) {
32 | gchar *new_str = NULL;
33 |
34 | if (g_utf8_validate(str, -1, NULL) && g_utf8_validate(chars, -1, NULL) &&
35 | pos >= 0 && pos <= g_utf8_strlen(str, -1)) {
36 | gchar *from = g_utf8_substring(str, 0, pos);
37 | gchar *end = g_utf8_offset_to_pointer(str, pos);
38 |
39 | new_str = g_strconcat(from, chars, end, NULL);
40 |
41 | g_free(from);
42 |
43 | } else {
44 | new_str = g_new0(gchar, 1);
45 | }
46 |
47 | return new_str;
48 | }
49 |
50 | glong string_get_nb_bytes_until(gchar *str, glong until) {
51 | glong ret = 0;
52 | if (str) {
53 | gchar *sub = g_utf8_substring(str, 0, until);
54 | ret = strlen(sub);
55 | g_free(sub);
56 | }
57 |
58 | return ret;
59 | }
60 |
61 | void pixel_data_print(guint32 pixel) {
62 | const guint32 r = pixel >> 24 & 0xff;
63 | const guint32 g = pixel >> 16 & 0xff;
64 | const guint32 b = pixel >> 8 & 0xff;
65 | const guint32 a = pixel >> 0 & 0xff;
66 |
67 | g_debug("rgba(%u, %d, %u, %u)", r, g, b, a);
68 | }
69 |
--------------------------------------------------------------------------------
/swappy.1.scd:
--------------------------------------------------------------------------------
1 | swappy(1)
2 |
3 | # NAME
4 |
5 | swappy - grab and edit on the fly snapshots of a Wayland compositor
6 |
7 | # SYNOPSIS
8 |
9 | *swappy* [options...]
10 |
11 | # DESCRIPTION
12 |
13 | swappy is a command-line utility to take and edit screenshots of Wayland
14 | desktops. Works great with grim, slurp and sway. But can easily work with
15 | other screen copy tools that can output a final image to *stdout*.
16 |
17 | swappy will save the annotated images to the config *save_dir*, see below.
18 |
19 | If absent, then if it will try to default to a *Desktop* folder following this
20 | pattern: *$XDG\_DESKTOP\_DIR*. If this variable is not set, it will revert to:
21 | *$XDG\_CONFIG\_HOME/Desktop*. If *$XDG\_CONFIG\_HOME* is not set, it will revert
22 | to: *$HOME/Desktop*.
23 |
24 | # OPTIONS
25 |
26 | *-h, --help*
27 | Show help message and quit.
28 |
29 | *-v, --version*
30 | Show version and quit.
31 |
32 | *-f, --file*
33 | An image file to load for editing.
34 |
35 | If set to *-*, read the file from standard input instead. This is grim
36 | friendly.
37 |
38 | *-o, --output-file *
39 | Print the final surface to ** when exiting the application.
40 |
41 | If set to *-*, prints the final surface to *stdout*.
42 |
43 | Note that the *Save* button will save the image to the config *save_dir*
44 | parameter, as described in the DESCRIPTION section.
45 |
46 | # CONFIG FILE
47 |
48 | The config file is located at *$XDG\_CONFIG\_HOME/swappy/config* or at
49 | *$HOME/.config/swappy/config*. The file follows the GLib *conf* format.
50 |
51 | ```
52 | [Section]
53 | key=value
54 | ```
55 |
56 | The following lines can be used as swappy's default:
57 |
58 | ```
59 | [Default]
60 | save_dir=$HOME/Desktop
61 | save_filename_format=swappy-%Y%m%d-%H%M%S.png
62 | show_panel=false
63 | line_size=5
64 | text_size=20
65 | text_font=sans-serif
66 | paint_mode=brush
67 | early_exit=false
68 | fill_shape=false
69 | auto_save=false
70 | custom_color=rgba(192,125,17,1)
71 | transparent=false
72 | transparency=50
73 | ```
74 |
75 | - *save_dir* is where swappshots will be saved, can contain env variables, when it does not exist, swappy attempts to create it first, but does not abort if directory creation fails
76 | - *save_filename_format* is the filename template, if it contains a date format, this will be parsed into a timestamp. Format is detailed in strftime(3). If this date format is missing, filename will have no timestamp
77 | - *show_panel* is used to toggle the paint panel on or off upon startup
78 | - *line_size* is the default line size (must be between 1 and 50)
79 | - *text_size* is the default text size (must be between 10 and 50)
80 | - *text_font* is the font used to render text, its format is pango friendly
81 | - *paint_mode* is the mode activated at application start (must be one of: brush|text|rectangle|ellipse|arrow|blur, matching is case-insensitive)
82 | - *early_exit* is used to make the application exit after saving the picture or copying it to the clipboard
83 | - *fill_shape* is used to toggle shape filling (for the rectangle and ellipsis tools) on or off upon startup
84 | - *auto_save* is used to toggle auto saving of final buffer to *save_dir* upon exit
85 | - *custom_color* is used to set a default value for the custom color. Accepted
86 | formats are: standard name (one of: https://github.com/rgb-x/system/blob/master/root/etc/X11/rgb.txt), #rgb, #rrggbb, #rrrgggbbb, #rrrrggggbbbb, rgb(r,b,g), rgba(r,g,b,a)
87 | - *transparency* is used to set transparency of everything that is drawn during startup
88 | - *transparent* is used to toggle transparency during startup
89 |
90 |
91 | # KEY BINDINGS
92 |
93 | ## LAYOUT
94 |
95 | - *Ctrl+b*: Toggle Paint Panel
96 |
97 | ## PAINT MODE
98 |
99 | - *b*: Switch to Brush
100 | - `e` `t`: Switch to Text (Editor)
101 | - `r` `s`: Switch to Rectangle (Square)
102 | - `c` `o`: Switch to Ellipse (Circle)
103 | - *a*: Switch to Arrow
104 | - *d*: Switch to Blur (d stands for droplet)
105 |
106 | - *R*: Use Red Color
107 | - *G*: Use Green Color
108 | - *B*: Use Blue Color
109 | - *C*: Use Custom Color
110 | - *Minus*: Reduce Stroke Size
111 | - *Plus*: Increase Stroke Size
112 | - *Equal*: Reset Stroke Size
113 | - *f*: Toggle Shape Filling
114 | - *T*: Toggle Transparency
115 | - `x` `k`: Clear Paints (cannot be undone)
116 |
117 | ## MODIFIERS
118 |
119 | - *Ctrl*: Center Shape (Rectangle & Ellipse) based on draw start
120 |
121 | ## HEADER BAR
122 |
123 | - *Ctrl+z*: Undo
124 | - *Ctrl+Shift+z* or *Ctrl+y*: Redo
125 | - *Ctrl+s*: Save to file (see man page)
126 | - *Ctrl+c*: Copy to clipboard
127 | - *Escape* or *q* or *Ctrl+w*: Quit swappy
128 |
129 | # AUTHORS
130 |
131 | Written and maintained by jtheoof . See
132 | https://github.com/jtheoof/swappy.
133 |
--------------------------------------------------------------------------------
/test/images/heart-transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/test/images/heart-transparent.png
--------------------------------------------------------------------------------
/test/images/large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/test/images/large.png
--------------------------------------------------------------------------------
/test/images/passwords.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/test/images/passwords.png
--------------------------------------------------------------------------------
/test/images/small-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/test/images/small-blue.png
--------------------------------------------------------------------------------