├── .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
├── 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 | AnalyzeTemporaryDtors: false
6 | FormatStyle: none
7 | User: jattali
8 | CheckOptions:
9 | - key: cert-dcl16-c.NewSuffixes
10 | value: 'L;LL;LU;LLU'
11 | - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField
12 | value: '0'
13 | - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors
14 | value: '1'
15 | - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
16 | value: '1'
17 | - key: google-readability-braces-around-statements.ShortStatementLines
18 | value: '1'
19 | - key: google-readability-function-size.StatementThreshold
20 | value: '800'
21 | - key: google-readability-namespace-comments.ShortNamespaceLines
22 | value: '10'
23 | - key: google-readability-namespace-comments.SpacesBeforeComments
24 | value: '2'
25 | - key: modernize-loop-convert.MaxCopySize
26 | value: '16'
27 | - key: modernize-loop-convert.MinConfidence
28 | value: reasonable
29 | - key: modernize-loop-convert.NamingStyle
30 | value: CamelCase
31 | - key: modernize-pass-by-value.IncludeStyle
32 | value: llvm
33 | - key: modernize-replace-auto-ptr.IncludeStyle
34 | value: llvm
35 | - key: modernize-use-nullptr.NullMacros
36 | value: 'NULL'
37 | ...
38 |
39 |
--------------------------------------------------------------------------------
/.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-gcc:
7 | runs-on: ubuntu-20.04
8 | steps:
9 | - uses: actions/checkout@v1
10 | - name: GCC
11 | run: |
12 | sudo apt-get update
13 | sudo apt --yes install libgtk-3-dev meson ninja-build scdoc
14 | pkg-config --list-all
15 | CC=gcc meson setup build
16 | ninja -C build
17 |
18 | build-clang:
19 | runs-on: ubuntu-20.04
20 | steps:
21 | - uses: actions/checkout@v1
22 | - name: Clang
23 | run: |
24 | sudo apt-get update
25 | sudo apt --yes install libgtk-3-dev meson ninja-build scdoc clang clang-format clang-tidy
26 | CC=clang meson setup build
27 | ninja -C build
28 | echo "Making sure clang-format is correct..."
29 | git ls-files -- '*.[ch]' | xargs clang-format -Werror -n
30 | echo "Running clang-tidy..."
31 | run-clang-tidy -p build
32 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on: [pull_request]
3 |
4 | jobs:
5 | commitlint:
6 | runs-on: ubuntu-20.04
7 | name: "Commit"
8 | env:
9 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
10 | steps:
11 | - uses: actions/checkout@v2
12 | with:
13 | fetch-depth: 0
14 | - uses: wagoid/commitlint-github-action@v1
15 | with:
16 | configFile: "./.commitlintrc.yml"
17 | firstParent: false
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 |
--------------------------------------------------------------------------------
/.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.5.1](https://github.com/jtheoof/swappy/compare/v1.5.0...v1.5.1) (2022-11-20)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * **ui:** use *-symbolic variant of toolbar icons ([5dc44f8](https://github.com/jtheoof/swappy/commit/5dc44f8970b0f6cdf21466bc2689ec2aa93a4385)), closes [#34](https://github.com/jtheoof/swappy/issues/34)
11 |
12 | ## [1.5.0](https://github.com/jtheoof/swappy/compare/v1.4.0...v1.5.0) (2022-11-18)
13 |
14 |
15 | ### Features
16 |
17 | * **config:** add early_exit option ([60da549](https://github.com/jtheoof/swappy/commit/60da5491e243c9edd85f6225326a68ae5e3edfd5))
18 | * **config:** allow paint_mode to be configured through config file ([2f35f02](https://github.com/jtheoof/swappy/commit/2f35f02b4e89bf67b6e9cc461e874331d8ce2a4c))
19 | * **config:** try to create `save_dir` if it does not exist ([4fb291a](https://github.com/jtheoof/swappy/commit/4fb291ad4b0b116afeaa7094b040083111b74674))
20 | * **ui:** allow filling rectangles and ellipsis ([8ee55f7](https://github.com/jtheoof/swappy/commit/8ee55f7d52ce6ac71752981863f5795fef460049)), closes [#120](https://github.com/jtheoof/swappy/issues/120)
21 |
22 | ## [1.4.0](https://github.com/jtheoof/swappy/compare/v1.3.1...v1.4.0) (2021-09-06)
23 |
24 |
25 | ### Features
26 |
27 | * **draw:** draw shape from center if holding control ([d80c361](https://github.com/jtheoof/swappy/commit/d80c3614895d3b5da479831c651cc1afa2fcf916))
28 | * **i18n:** add french translations ([cacb283](https://github.com/jtheoof/swappy/commit/cacb2830e4cc41010d6ab96655054d2eb1651651))
29 |
30 |
31 | ### Bug Fixes
32 |
33 | * **desktop:** remove annotation from desktop categories ([0d383f6](https://github.com/jtheoof/swappy/commit/0d383f690b99026c340eab1efa590c48d54e7368))
34 | * **desktop:** various fixes ([42425c0](https://github.com/jtheoof/swappy/commit/42425c0657a65b3f66ba4f64b1727c8198a70684))
35 | * **i18n:** add german translations to desktop file ([c6b09e5](https://github.com/jtheoof/swappy/commit/c6b09e56399369b14a8de090a2239350dbe4aca8))
36 | * **i18n:** add turkish translation to desktop file ([fa5769e](https://github.com/jtheoof/swappy/commit/fa5769e9406b8ab1b67aca3bff2656850362491e))
37 | * **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)
38 | * **pixbuf:** handle invalid input file ([cdbd06d](https://github.com/jtheoof/swappy/commit/cdbd06d7af94b4aedfc2bda2231da8853f775f3a))
39 | * **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)
40 | * **po:** update GETTEXT_PACKAGE value with project name ([7fd552e](https://github.com/jtheoof/swappy/commit/7fd552e8c41f29711212d7f70edf61ac6ada7a7d))
41 | * **release:** properly check sha256 remote content ([91985c7](https://github.com/jtheoof/swappy/commit/91985c7994764f52c8e9d864db8ec9cf2eb1df5c)), closes [#90](https://github.com/jtheoof/swappy/issues/90)
42 |
43 | ### [1.3.1](https://github.com/jtheoof/swappy/compare/v1.3.0...v1.3.1) (2021-02-20)
44 |
45 | ## [1.3.0](https://github.com/jtheoof/swappy/compare/v1.2.1...v1.3.0) (2021-02-18)
46 |
47 |
48 | ### Features
49 |
50 | * **cli:** add configure options for filename save ([597f005](https://github.com/jtheoof/swappy/commit/597f0055b9c6230b25a7f7a7bf3f4e14c06b1fbb))
51 | * **i18n:** add brazilian portuguese translations ([4a0eb82](https://github.com/jtheoof/swappy/commit/4a0eb82369a0859fafdcce9d242c086cd2360a84))
52 | * **i18n:** add german translations ([b4be847](https://github.com/jtheoof/swappy/commit/b4be8476350771454b29b9ce29c62a3337acc736))
53 | * **i18n:** add turkish translations ([c8419da](https://github.com/jtheoof/swappy/commit/c8419da7faef14223ada6853942a6d11e2acf92f))
54 |
55 |
56 | ### Bug Fixes
57 |
58 | * **application:** unlink temp file coming from stdin ([c24e56a](https://github.com/jtheoof/swappy/commit/c24e56a165394e60b37534287e168e5d8e69627c)), closes [#80](https://github.com/jtheoof/swappy/issues/80)
59 | * **blur:** optimize blur to only render after commit ([27fcece](https://github.com/jtheoof/swappy/commit/27fcecedaeea49aaec6acdecbc51cbd865a13363))
60 | * **blur:** rgb24 is properly handled ([c04ed63](https://github.com/jtheoof/swappy/commit/c04ed63d26e5012215198f7b41a7f2232dac1ebe))
61 | * **clipboard:** wl-copy mimetype should be png ([a931acb](https://github.com/jtheoof/swappy/commit/a931acb2cff615badc63294ed121aba008f32ef8)), closes [#68](https://github.com/jtheoof/swappy/issues/68)
62 | * **notification:** notification shows the image icon ([eb53e5c](https://github.com/jtheoof/swappy/commit/eb53e5c2b28717f509dd58eab6da85897c0d6d9d))
63 | * **ui:** adjust rendering surface with proper scaling ([9b72571](https://github.com/jtheoof/swappy/commit/9b72571596f9313d4efd94a4b17da8b3733fd2de)), closes [#54](https://github.com/jtheoof/swappy/issues/54)
64 | * **ui:** commit state before copying or saving ([46e5854](https://github.com/jtheoof/swappy/commit/46e5854b3cce93a82984b19ca90e3f3337952fe2)), closes [#52](https://github.com/jtheoof/swappy/issues/52)
65 | * **ui:** compute window sizes and buffers properly ([5bcffdb](https://github.com/jtheoof/swappy/commit/5bcffdbb01cc6e56f9c0f37de899b46efe68ed4a)), closes [#56](https://github.com/jtheoof/swappy/issues/56)
66 |
67 | ### [1.2.1](https://github.com/jtheoof/swappy/compare/v1.2.0...v1.2.1) (2020-07-11)
68 |
69 |
70 | ### Bug Fixes
71 |
72 | * **text:** properly handle utf-8 chars ([717ab0c](https://github.com/jtheoof/swappy/commit/717ab0c2d1757e10bb4eef17d35ccd6a991705c4)), closes [#43](https://github.com/jtheoof/swappy/issues/43)
73 |
74 | ## [1.2.0](https://github.com/jtheoof/swappy/compare/v1.1.0...v1.2.0) (2020-07-05)
75 |
76 |
77 | ### Features
78 |
79 | * **i18n:** add translatable desktop file ([cf3d7a5](https://github.com/jtheoof/swappy/commit/cf3d7a5283a7b8c34b05996f87b608513e0830ca)), closes [#35](https://github.com/jtheoof/swappy/issues/35)
80 | * **i18n:** setup i18n for swappy ([5b3c8ad](https://github.com/jtheoof/swappy/commit/5b3c8aded8fd4f9d00aa660a24127de0e1791d7f))
81 |
82 | ## [1.2.0](https://github.com/jtheoof/swappy/compare/v1.1.0...v1.2.0) (2020-07-05)
83 |
84 |
85 | ### Features
86 |
87 | * **i18n:** add translatable desktop file ([cf3d7a5](https://github.com/jtheoof/swappy/commit/cf3d7a5283a7b8c34b05996f87b608513e0830ca)), closes [#35](https://github.com/jtheoof/swappy/issues/35)
88 | * **i18n:** setup i18n for swappy ([5b3c8ad](https://github.com/jtheoof/swappy/commit/5b3c8aded8fd4f9d00aa660a24127de0e1791d7f))
89 |
90 | ## [1.1.0](https://github.com/jtheoof/swappy/compare/v1.0.1...v1.1.0) (2020-06-23)
91 |
92 |
93 | ### Features
94 |
95 | * **cli:** add -v and --version flags ([e32c024](https://github.com/jtheoof/swappy/commit/e32c02454ae4ec6ac30549d5fa9e80c2b64edb72))
96 |
97 | ### [1.0.1](https://github.com/jtheoof/swappy/compare/v1.0.0...v1.0.1) (2020-06-21)
98 |
99 |
100 | ### Bug Fixes
101 |
102 | * **cli:** stop showing -g option ([ee06d66](https://github.com/jtheoof/swappy/commit/ee06d6685f6f59ffce544b45d7b51f3f4523348b))
103 |
104 | ## 1.0.0 (2020-06-21)
105 |
106 |
107 | ### ⚠ BREAKING CHANGES
108 |
109 | * We do no support the `-g` option anymore.
110 |
111 | This tool simply makes more sense as the output of `grim` rather than
112 | trying to be `grim`.
113 |
114 | RIP my ugly wayland code, long live maintainable code.
115 |
116 | Next stop, rust?
117 |
118 | ### Features
119 |
120 | * **ui:** life is full of colors and joy ([a8c8be3](https://github.com/jtheoof/swappy/commit/a8c8be37ca996f3e1b752bca67eee594706bc08f))
121 | * init project ([efc3ecc](https://github.com/jtheoof/swappy/commit/efc3eccc9e21892a6b0979126a23d21d3d6a3b3d))
122 | * **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)
123 | * **application:** update app ([ce27741](https://github.com/jtheoof/swappy/commit/ce27741017554d6606e23434273f55476bc8ae37))
124 | * **blur:** add multiple passes logic ([f9737d7](https://github.com/jtheoof/swappy/commit/f9737d78c96a5d9f4566c94702c3ec4a41d9e219))
125 | * **blur:** remove blur configuration ([361be6a](https://github.com/jtheoof/swappy/commit/361be6aa8085143d9fd721e4c315c6b9e6fbdfca))
126 | * **blur:** use rect blur instead of brush ([1be7798](https://github.com/jtheoof/swappy/commit/1be7798a8bcfc494b20489e2e1f8b0245f4b5e84)), closes [#17](https://github.com/jtheoof/swappy/issues/17)
127 | * **buffer:** ability to read from stdin ([02bc464](https://github.com/jtheoof/swappy/commit/02bc46456453e8530a3c9f1289dfce7e71371945))
128 | * **buffer:** add file image support ([f6c189c](https://github.com/jtheoof/swappy/commit/f6c189c7b7f35ca4da75abaac0bd85c3d5ce5b09))
129 | * **clipboard:** use wl-copy if present ([51b27d7](https://github.com/jtheoof/swappy/commit/51b27d768eef7fbbdab365fa94a81af5395b0e3e))
130 | * **config:** add show_panel config ([307f579](https://github.com/jtheoof/swappy/commit/307f57956f105d22de2d8242313517b6a79ed4e2)), closes [#12](https://github.com/jtheoof/swappy/issues/12)
131 | * **config:** have overridable defaults ([ef24851](https://github.com/jtheoof/swappy/commit/ef24851deec2d6b7f76ed0fbbcd31b54b336cae3)), closes [#1](https://github.com/jtheoof/swappy/issues/1)
132 | * **draw:** convert wl_shm_format to cairo_format ([c623939](https://github.com/jtheoof/swappy/commit/c623939e02238f053312ad6367e761aec254c6fe))
133 | * **draw:** draw the screencopy buffer ([2344414](https://github.com/jtheoof/swappy/commit/2344414102789975e6ce425a95e8b96159cf51ba))
134 | * **layer:** use geometry size ([290d3ca](https://github.com/jtheoof/swappy/commit/290d3ca230d32ec2ef4036bf9e32f1e711fecd84))
135 | * **paint:** introduce text paint ([3347bf2](https://github.com/jtheoof/swappy/commit/3347bf23bf17d4c2cc8e5b9bbadd657efafb28e7))
136 | * **screencopy:** add buffer creation through screencopy ([bff8687](https://github.com/jtheoof/swappy/commit/bff8687fc81ebb57a179b1f50300f9c0cda793e3))
137 | * **screencopy:** introduce screencopy features ([53c9770](https://github.com/jtheoof/swappy/commit/53c977080829c7e816db1a9ec45eb432f6b7b354))
138 | * **swappy:** copy to clipboard with CTRL+C ([b90500e](https://github.com/jtheoof/swappy/commit/b90500ed34defcb8ebc67965c4dbb5d068ee8049))
139 | * **swappy:** introduce file option ([c56df33](https://github.com/jtheoof/swappy/commit/c56df33d1880d22372e21ef0ebf5dd8805d65a76))
140 | * **swappy:** save to file with CTRL+S ([af0b1a1](https://github.com/jtheoof/swappy/commit/af0b1a11a21faac04f8b43c4c9ef616ab5fd2b78))
141 | * **text:** add controls in toggle panel ([c03f628](https://github.com/jtheoof/swappy/commit/c03f628de793e170d9f62c5b786fe18891bb6fa3))
142 | * **tool:** introduce blurring capability ([fae0aea](https://github.com/jtheoof/swappy/commit/fae0aeacab6fb28e17975097c8b4c5c7e5ad57fd)), closes [#17](https://github.com/jtheoof/swappy/issues/17)
143 | * **ui:** add binding for clear action ([2bdab68](https://github.com/jtheoof/swappy/commit/2bdab684e1eace53ad7b78414ad467d312dc10ad))
144 | * **ui:** add binding to toggle panel ([e8d2f12](https://github.com/jtheoof/swappy/commit/e8d2f12ce1737fa19972e5c4109e1c85cc2b157e))
145 | * **ui:** add keybindings for color change ([c5ec285](https://github.com/jtheoof/swappy/commit/c5ec285ee73ddf90df2cb571e1d6c61159605c8e))
146 | * **ui:** add keybindings for stroke size ([562a9a6](https://github.com/jtheoof/swappy/commit/562a9a6e92201677f31de126b646c619caf33863))
147 | * **ui:** add shortcuts for undo/redo ([d7e7f2b](https://github.com/jtheoof/swappy/commit/d7e7f2b5ffd46aa36bed6ecc6709aeb94cce64ae))
148 | * **ui:** add toggle panel button ([7674d7d](https://github.com/jtheoof/swappy/commit/7674d7db8ba8d97302a045af8d2383de37acb2d1)), closes [#24](https://github.com/jtheoof/swappy/issues/24)
149 | * **ui:** add undo/redo ([bcc1314](https://github.com/jtheoof/swappy/commit/bcc13140ebfdefa30431b288f089d23bb1df743e))
150 | * **ui:** life is full of colors and joy ([606cd34](https://github.com/jtheoof/swappy/commit/606cd3459de3908e5fecdb7a49162ef3a9b52ab7))
151 | * **ui:** replace popover by on screen elements ([8cd3f13](https://github.com/jtheoof/swappy/commit/8cd3f134bbd8e05523303914f6c8f3989e6b4502))
152 | * **wayland:** added xdg_output_manager ([7b3549f](https://github.com/jtheoof/swappy/commit/7b3549fdd86fe1a945e1988bf22042c0f8dd6ed0))
153 | * **wayland:** listing outputs ([5a55c8b](https://github.com/jtheoof/swappy/commit/5a55c8bbbd08ad717ddabac51be31483950d827f))
154 |
155 |
156 | ### Bug Fixes
157 |
158 | * **application:** fix file loop and use of GTK object after lifecycle ([320dae0](https://github.com/jtheoof/swappy/commit/320dae02d0c6dca3fa2fd7ca934a85483ac2dd35))
159 | * **application:** memory leak for pixbuf ([f9d70fc](https://github.com/jtheoof/swappy/commit/f9d70fc0e22274e6cbe74bfdf714cdf04e34053d))
160 | * **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)
161 | * **application:** suffix saved file with png ([7f2f6da](https://github.com/jtheoof/swappy/commit/7f2f6da754571771475558233f5a47813ec278dd))
162 | * **blur:** adjust blur bounding box based on scaled monitor ([6b2ec90](https://github.com/jtheoof/swappy/commit/6b2ec90efd99e1979310b673ad40b3724669dac1))
163 | * **blur:** blur based on device scaling factor ([1699474](https://github.com/jtheoof/swappy/commit/1699474c39fc305492c8bb03063c4582af4dbf9e))
164 | * **blur:** use better glyph icon ([97cd607](https://github.com/jtheoof/swappy/commit/97cd6072c986c9a7c69306744390a6ddb6a44646))
165 | * **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)
166 | * **buffer:** properly include required functions ([d787586](https://github.com/jtheoof/swappy/commit/d787586b9ed1d7e855ae2d416914d619636f41b1)), closes [#10](https://github.com/jtheoof/swappy/issues/10)
167 | * **clipboard:** handle bad write to pipe fd ([f963a76](https://github.com/jtheoof/swappy/commit/f963a76c5c01b9b5f81b97118bf1b9e6990d995d))
168 | * **clipboard:** memory leak for pixbuf ([665295b](https://github.com/jtheoof/swappy/commit/665295b497d7ef124d5a2eeb7eb76964fdb3566a))
169 | * **dependencies:** include glib2 ([992d97e](https://github.com/jtheoof/swappy/commit/992d97e94d2ebd32ac3e1901910050fae1954ed0)), closes [#11](https://github.com/jtheoof/swappy/issues/11)
170 | * **file:** properly check file system errors if any ([541ec21](https://github.com/jtheoof/swappy/commit/541ec21ca0efdec4d06c96f5ad1768b4219ed4ab))
171 | * **init:** fix segfault for unknown flags ([f4e9a19](https://github.com/jtheoof/swappy/commit/f4e9a19407d8d1bfa59c08f6bf97617c662e1ac0))
172 | * **init:** properly handle null geometry ([c4ea305](https://github.com/jtheoof/swappy/commit/c4ea305ae6ac9429bf44fdfc7218a30363439582))
173 | * **man:** remove blur_level related config ([ceb907a](https://github.com/jtheoof/swappy/commit/ceb907a5dc736c7d44318b35fb911aeb2360d851))
174 | * **meson:** able to build on standard platforms ([8abc5d5](https://github.com/jtheoof/swappy/commit/8abc5d52ec2962a111c6d44cdb5e9e209ac219c7))
175 | * **meson:** remove useless cname in meson res file ([9b8ea64](https://github.com/jtheoof/swappy/commit/9b8ea64307b33eb010b8ba043919f3eddf935b19))
176 | * **paint:** fix memory leak for brush paints ([aed2bfe](https://github.com/jtheoof/swappy/commit/aed2bfe29465aa5161155c1edda9d03cac607906))
177 | * **pixbuf:** possibly fix core dump ([8a82e79](https://github.com/jtheoof/swappy/commit/8a82e796bb871b57fa6ab4d2ed8d761033370d8c))
178 | * **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)
179 | * **render:** better handler empty buffer ([acf2379](https://github.com/jtheoof/swappy/commit/acf2379ba3117ba6eb8c426e85a60ce71a3abe67))
180 | * **render:** draw from last to first ([4b69ada](https://github.com/jtheoof/swappy/commit/4b69ada9a1469d3b6e106e07bf7155836b31d613))
181 | * **render:** fix arrow glitch with 0 ftx ([ec6e6ab](https://github.com/jtheoof/swappy/commit/ec6e6abae7629800fec4c715957c4932946f51ed))
182 | * **render:** properly scale arrow along with stroke size ([75bfc10](https://github.com/jtheoof/swappy/commit/75bfc10fb7a5507b66bd6d19ab06f2f6a393bb6a))
183 | * **resources:** compile resources and fix error management ([05d87c9](https://github.com/jtheoof/swappy/commit/05d87c929ff8b3311cd5db111cd2f53a32c35a19))
184 | * **string:** fix algo to insert chars at location ([bc3264e](https://github.com/jtheoof/swappy/commit/bc3264e9f11bb4f3a02d7f5ae92ef8a4d2b42513))
185 | * **ui:** add stroke size increase/decrease/reset ([5930c99](https://github.com/jtheoof/swappy/commit/5930c99b9e0208148d6bc8cf0fc3aa8f69dbd36d))
186 | * **ui:** move paint area inside GtkFixed ([50e7c97](https://github.com/jtheoof/swappy/commit/50e7c97042805f5550d2a62d45c8e49208d7632d))
187 | * **ui:** prevent focus in panel buttons ([903ad11](https://github.com/jtheoof/swappy/commit/903ad114f516981c8d0644f704af9c722f74a61f))
188 | * **ui:** small tweaks ([2b73777](https://github.com/jtheoof/swappy/commit/2b73777142141598c14d37d1b6fa9573de12d914))
189 | * **ui:** tweak button sizes ([425f455](https://github.com/jtheoof/swappy/commit/425f455ab7665a046060fe140c861aeb7ea8209b))
190 | * **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)
191 | * **wayland:** initialize done copies to 0 ([65cefc1](https://github.com/jtheoof/swappy/commit/65cefc1da7fed86508301250ffc1b6dbc9fd3692))
192 | * **wayland:** replace g_error by g_warning ([64bfc2b](https://github.com/jtheoof/swappy/commit/64bfc2b3a71ed00d0dc1102501ac85792735833f))
193 | * **window:** quit when delete event is received ([0c5e458](https://github.com/jtheoof/swappy/commit/0c5e458d4c44a2e2e2b4451b4576724aef2a06b0))
194 |
195 |
196 | * refactor!(wayland): remove wayland code ([204a93e](https://github.com/jtheoof/swappy/commit/204a93eb0f696bc7be8335d46212c6024e3b2c51))
197 |
--------------------------------------------------------------------------------
/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 | ```
57 |
58 | - `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
59 | - `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
60 | - `show_panel` is used to toggle the paint panel on or off upon startup
61 | - `line_size` is the default line size (must be between 1 and 50)
62 | - `text_size` is the default text size (must be between 10 and 50)
63 | - `text_font` is the font used to render text, its format is pango friendly
64 | - `paint_mode` is the mode activated at application start (must be one of: brush|text|rectangle|ellipse|arrow|blur, matching is case-insensitive)
65 | - `early_exit` is used to make the application exit after saving the picture or copying it to the clipboard
66 | - `fill_shape` is used to toggle shape filling (for the rectangle and ellipsis tools) on or off upon startup
67 | - `auto_save` is used to toggle auto saving of final buffer to `save_dir` upon exit
68 | - `custom_color` is used to set a default value for the custom color
69 |
70 |
71 | ## Keyboard Shortcuts
72 |
73 | - `Ctrl+b`: Toggle Paint Panel
74 |
75 |
76 |
77 | - `b`: Switch to Brush
78 | - `t`: Switch to Text
79 | - `r`: Switch to Rectangle
80 | - `o`: Switch to Ellipse
81 | - `a`: Switch to Arrow
82 | - `d`: Switch to Blur (`d` stands for droplet)
83 |
84 |
85 |
86 | - `R`: Use Red Color
87 | - `G`: Use Green Color
88 | - `B`: Use Blue Color
89 | - `C`: Use Custom Color
90 | - `Minus`: Reduce Stroke Size
91 | - `Plus`: Increase Stroke Size
92 | - `Equal`: Reset Stroke Size
93 | - `f`: Toggle Shape Filling
94 | - `k`: Clear Paints (cannot be undone)
95 |
96 |
97 |
98 | - `Ctrl`: Center Shape (Rectangle & Ellipse) based on draw start
99 |
100 |
101 |
102 | - `Ctrl+z`: Undo
103 | - `Ctrl+Shift+z` or `Ctrl+y`: Redo
104 | - `Ctrl+s`: Save to file (see man page)
105 | - `Ctrl+c`: Copy to clipboard
106 | - `Escape` or `q` or `Ctrl+w`: Quit swappy
107 |
108 | ## Limitations
109 |
110 | - **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.
111 | - **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`
112 |
113 | ## Installation
114 |
115 | - [Arch Linux](https://archlinux.org/packages/extra/x86_64/swappy/)
116 | - [Arch Linux (git)](https://aur.archlinux.org/packages/swappy-git)
117 | - [Fedora](https://src.fedoraproject.org/rpms/swappy)
118 | - [Gentoo](https://packages.gentoo.org/packages/gui-apps/swappy)
119 | - [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/swappy)
120 | - [Void Linux](https://github.com/void-linux/void-packages/tree/master/srcpkgs/swappy)
121 |
122 | ## Building from source
123 |
124 | Install dependencies (on Arch, name can vary for other distros):
125 |
126 | - meson
127 | - ninja
128 | - cairo
129 | - pango
130 | - gtk
131 | - glib2
132 | - scdoc
133 |
134 | Optional dependencies:
135 |
136 | - `wl-clipboard` (to make sure the copy is saved if you close swappy)
137 | - `otf-font-awesome` (to draw the paint icons properly)
138 |
139 | Then run:
140 |
141 | ```sh
142 | meson setup build
143 | ninja -C build
144 | ```
145 |
146 | ### i18n
147 |
148 | This section is for developers, maintainers and translators.
149 |
150 | To add support to a new locale or when translations are updated:
151 |
152 | 1. Update `src/po/LINGUAS` (when new locales are added)
153 | 2. Generate a new `po` file (ignore and do not commit potential noise in other files):
154 |
155 | ```sh
156 | ninja -C build swappy-update-po
157 | ```
158 |
159 | To rebuild the base template (should happen less often):
160 |
161 | ```sh
162 | ninja -C build swappy-pot
163 | ```
164 |
165 | See the [meson documentation](https://mesonbuild.com/Localisation.html) for details.
166 |
167 | ## Contributing
168 |
169 | Pull requests are welcome. This project uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to automate changelog generation.
170 |
171 | ## Release
172 |
173 | 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.
174 |
175 | ```sh
176 | ./script/github-release
177 | ```
178 |
179 | Make sure everything is valid in the Draft release, then publish the draft.
180 |
181 | Release tarballs are signed with this PGP key: `F44D05A50F6C9EB5C81BCF966A6B35DBE9442683`
182 |
183 | ## License
184 |
185 | MIT
186 |
187 | [snappy]: http://snappy-app.com/
188 | [slurp]: https://github.com/emersion/slurp
189 | [grim]: https://github.com/emersion/grim
190 | [sway]: https://github.com/swaywm/sway
191 | [wl-clipboard]: https://github.com/bugaevc/wl-clipboard
192 |
--------------------------------------------------------------------------------
/docs/images/screenshot-1.0.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/docs/images/screenshot-1.0.0.png
--------------------------------------------------------------------------------
/docs/images/screenshot-beta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/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 |
--------------------------------------------------------------------------------
/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_SHOW_PANEL_DEFAULT false
7 | #define CONFIG_SAVE_FILENAME_FORMAT_DEFAULT "swappy-%Y%m%d_%H%M%S.png"
8 | #define CONFIG_PAINT_MODE_DEFAULT SWAPPY_PAINT_MODE_BRUSH
9 | #define CONFIG_EARLY_EXIT_DEFAULT false
10 | #define CONFIG_FILL_SHAPE_DEFAULT false
11 | #define CONFIG_AUTO_SAVE_DEFAULT false
12 | #define CONFIG_CUSTOM_COLOR_DEFAULT "rgba(193,125,17,1)"
13 |
14 | void config_load(struct swappy_state *state);
15 | void config_free(struct swappy_state *state);
16 |
--------------------------------------------------------------------------------
/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_text_clip(struct swappy_state *state, gdouble x,
14 | gdouble y);
15 | void paint_commit_temporary(struct swappy_state *state);
16 |
17 | void paint_free(gpointer data);
18 | void paint_free_all(struct swappy_state *state);
19 | void paint_free_list(GList **list);
20 |
--------------------------------------------------------------------------------
/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 | enum swappy_paint_type {
18 | SWAPPY_PAINT_MODE_BRUSH = 0, /* Brush mode to draw arbitrary shapes */
19 | SWAPPY_PAINT_MODE_TEXT, /* Mode to draw texts */
20 | SWAPPY_PAINT_MODE_RECTANGLE, /* Rectangle shapes */
21 | SWAPPY_PAINT_MODE_ELLIPSE, /* Ellipse shapes */
22 | SWAPPY_PAINT_MODE_ARROW, /* Arrow shapes */
23 | SWAPPY_PAINT_MODE_BLUR, /* Blur mode */
24 | };
25 |
26 | enum swappy_paint_shape_operation {
27 | SWAPPY_PAINT_SHAPE_OPERATION_STROKE = 0, /* Used to stroke the shape */
28 | SWAPPY_PAINT_SHAPE_OPERATION_FILL, /* Used to fill the shape */
29 | };
30 |
31 | enum swappy_text_mode {
32 | SWAPPY_TEXT_MODE_EDIT = 0,
33 | SWAPPY_TEXT_MODE_DONE,
34 | };
35 |
36 | struct swappy_point {
37 | gdouble x;
38 | gdouble y;
39 | };
40 |
41 | struct swappy_paint_text {
42 | double r;
43 | double g;
44 | double b;
45 | double a;
46 | double s;
47 | gchar *font;
48 | gchar *text;
49 | glong cursor;
50 | struct swappy_point from;
51 | struct swappy_point to;
52 | enum swappy_text_mode mode;
53 | };
54 |
55 | struct swappy_paint_shape {
56 | double r;
57 | double g;
58 | double b;
59 | double a;
60 | double w;
61 | bool should_center_at_from;
62 | struct swappy_point from;
63 | struct swappy_point to;
64 | enum swappy_paint_type type;
65 | enum swappy_paint_shape_operation operation;
66 | };
67 |
68 | struct swappy_paint_brush {
69 | double r;
70 | double g;
71 | double b;
72 | double a;
73 | double w;
74 | GList *points;
75 | };
76 |
77 | struct swappy_paint_blur {
78 | struct swappy_point from;
79 | struct swappy_point to;
80 | cairo_surface_t *surface;
81 | };
82 |
83 | struct swappy_paint {
84 | enum swappy_paint_type type;
85 | bool can_draw;
86 | bool is_committed;
87 | union {
88 | struct swappy_paint_brush brush;
89 | struct swappy_paint_shape shape;
90 | struct swappy_paint_text text;
91 | struct swappy_paint_blur blur;
92 | } content;
93 | };
94 |
95 | struct swappy_box {
96 | int32_t x;
97 | int32_t y;
98 | int32_t width;
99 | int32_t height;
100 | };
101 |
102 | struct swappy_state_settings {
103 | double r;
104 | double g;
105 | double b;
106 | double a;
107 | double w;
108 | double t;
109 | };
110 |
111 | struct swappy_state_ui {
112 | gboolean panel_toggled;
113 |
114 | GtkWindow *window;
115 | GtkWidget *area;
116 |
117 | GtkToggleButton *panel_toggle_button;
118 |
119 | // Undo / Redo
120 | GtkButton *undo;
121 | GtkButton *redo;
122 |
123 | // Painting Area
124 | GtkBox *painting_box;
125 | GtkRadioButton *brush;
126 | GtkRadioButton *text;
127 | GtkRadioButton *rectangle;
128 | GtkRadioButton *ellipse;
129 | GtkRadioButton *arrow;
130 | GtkRadioButton *blur;
131 |
132 | GtkRadioButton *red;
133 | GtkRadioButton *green;
134 | GtkRadioButton *blue;
135 | GtkRadioButton *custom;
136 | GtkColorButton *color;
137 |
138 | GtkButton *line_size;
139 | GtkButton *text_size;
140 |
141 | GtkToggleButton *fill_shape;
142 | };
143 |
144 | struct swappy_config {
145 | char *config_file;
146 | char *save_dir;
147 | char *save_filename_format;
148 | gint8 paint_mode;
149 | gboolean fill_shape;
150 | gboolean show_panel;
151 | guint32 line_size;
152 | guint32 text_size;
153 | char *text_font;
154 | gboolean early_exit;
155 | gboolean auto_save;
156 | char *custom_color;
157 | };
158 |
159 | struct swappy_state {
160 | GtkApplication *app;
161 |
162 | struct swappy_state_ui *ui;
163 | struct swappy_config *config;
164 |
165 | GdkPixbuf *original_image;
166 | cairo_surface_t *original_image_surface;
167 | cairo_surface_t *rendering_surface;
168 |
169 | gdouble scaling_factor;
170 |
171 | enum swappy_paint_type mode;
172 |
173 | /* Options */
174 | char *file_str;
175 | char *output_file;
176 |
177 | char *temp_file_str;
178 |
179 | struct swappy_box *window;
180 | struct swappy_box *geometry;
181 |
182 | GList *paints;
183 | GList *redo_paints;
184 | struct swappy_paint *temp_paint;
185 |
186 | struct swappy_state_settings settings;
187 |
188 | int argc;
189 | char **argv;
190 | };
191 |
--------------------------------------------------------------------------------
/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.5.1',
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.5.1",
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.glade:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
820 |
821 |
--------------------------------------------------------------------------------
/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 |
8 | #include "clipboard.h"
9 | #include "config.h"
10 | #include "file.h"
11 | #include "paint.h"
12 | #include "pixbuf.h"
13 | #include "render.h"
14 | #include "swappy.h"
15 |
16 | static void update_ui_undo_redo(struct swappy_state *state) {
17 | GtkWidget *undo = GTK_WIDGET(state->ui->undo);
18 | GtkWidget *redo = GTK_WIDGET(state->ui->redo);
19 | gboolean undo_sensitive = g_list_length(state->paints) > 0;
20 | gboolean redo_sensitive = g_list_length(state->redo_paints) > 0;
21 | gtk_widget_set_sensitive(undo, undo_sensitive);
22 | gtk_widget_set_sensitive(redo, redo_sensitive);
23 | }
24 |
25 | static void update_ui_stroke_size_widget(struct swappy_state *state) {
26 | GtkButton *button = GTK_BUTTON(state->ui->line_size);
27 | char label[255];
28 | g_snprintf(label, 255, "%.0lf", state->settings.w);
29 | gtk_button_set_label(button, label);
30 | }
31 |
32 | static void update_ui_text_size_widget(struct swappy_state *state) {
33 | GtkButton *button = GTK_BUTTON(state->ui->text_size);
34 | char label[255];
35 | g_snprintf(label, 255, "%.0lf", state->settings.t);
36 | gtk_button_set_label(button, label);
37 | }
38 |
39 | static void update_ui_panel_toggle_button(struct swappy_state *state) {
40 | GtkWidget *painting_box = GTK_WIDGET(state->ui->painting_box);
41 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->panel_toggle_button);
42 | gboolean toggled = state->ui->panel_toggled;
43 |
44 | gtk_toggle_button_set_active(button, toggled);
45 | gtk_widget_set_visible(painting_box, toggled);
46 | }
47 |
48 | static void update_ui_fill_shape_toggle_button(struct swappy_state *state) {
49 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->fill_shape);
50 | gboolean toggled = state->config->fill_shape;
51 |
52 | gtk_toggle_button_set_active(button, toggled);
53 | }
54 |
55 | void application_finish(struct swappy_state *state) {
56 | g_debug("application finishing, cleaning up");
57 | paint_free_all(state);
58 | pixbuf_free(state);
59 | cairo_surface_destroy(state->rendering_surface);
60 | cairo_surface_destroy(state->original_image_surface);
61 | if (state->temp_file_str) {
62 | g_info("deleting temporary file: %s", state->temp_file_str);
63 | if (g_unlink(state->temp_file_str) != 0) {
64 | g_warning("unable to delete temporary file: %s", state->temp_file_str);
65 | }
66 | g_free(state->temp_file_str);
67 | }
68 | g_free(state->file_str);
69 | g_free(state->geometry);
70 | g_free(state->window);
71 | g_free(state->ui);
72 |
73 | g_object_unref(state->app);
74 |
75 | config_free(state);
76 | }
77 |
78 | static void action_undo(struct swappy_state *state) {
79 | GList *first = state->paints;
80 |
81 | if (first) {
82 | state->paints = g_list_remove_link(state->paints, first);
83 | state->redo_paints = g_list_prepend(state->redo_paints, first->data);
84 |
85 | render_state(state);
86 | update_ui_undo_redo(state);
87 | }
88 | }
89 |
90 | static void action_redo(struct swappy_state *state) {
91 | GList *first = state->redo_paints;
92 |
93 | if (first) {
94 | state->redo_paints = g_list_remove_link(state->redo_paints, first);
95 | state->paints = g_list_prepend(state->paints, first->data);
96 |
97 | render_state(state);
98 | update_ui_undo_redo(state);
99 | }
100 | }
101 |
102 | static void action_clear(struct swappy_state *state) {
103 | paint_free_all(state);
104 | render_state(state);
105 | update_ui_undo_redo(state);
106 | }
107 |
108 | static void action_toggle_painting_panel(struct swappy_state *state,
109 | gboolean *toggled) {
110 | state->ui->panel_toggled =
111 | (toggled == NULL) ? !state->ui->panel_toggled : *toggled;
112 | update_ui_panel_toggle_button(state);
113 | }
114 |
115 | static void action_update_color_state(struct swappy_state *state, double r,
116 | double g, double b, double a,
117 | gboolean custom) {
118 | state->settings.r = r;
119 | state->settings.g = g;
120 | state->settings.b = b;
121 | state->settings.a = a;
122 |
123 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->color), custom);
124 | }
125 |
126 | static void action_set_color_from_custom(struct swappy_state *state) {
127 | GdkRGBA color;
128 | gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(state->ui->color), &color);
129 |
130 | action_update_color_state(state, color.red, color.green, color.blue,
131 | color.alpha, true);
132 | }
133 |
134 | static void switch_mode_to_brush(struct swappy_state *state) {
135 | state->mode = SWAPPY_PAINT_MODE_BRUSH;
136 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
137 | }
138 |
139 | static void switch_mode_to_text(struct swappy_state *state) {
140 | state->mode = SWAPPY_PAINT_MODE_TEXT;
141 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
142 | }
143 |
144 | static void switch_mode_to_rectangle(struct swappy_state *state) {
145 | state->mode = SWAPPY_PAINT_MODE_RECTANGLE;
146 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true);
147 | }
148 |
149 | static void switch_mode_to_ellipse(struct swappy_state *state) {
150 | state->mode = SWAPPY_PAINT_MODE_ELLIPSE;
151 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true);
152 | }
153 |
154 | static void switch_mode_to_arrow(struct swappy_state *state) {
155 | state->mode = SWAPPY_PAINT_MODE_ARROW;
156 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
157 | }
158 |
159 | static void switch_mode_to_blur(struct swappy_state *state) {
160 | state->mode = SWAPPY_PAINT_MODE_BLUR;
161 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
162 | }
163 |
164 | static void action_stroke_size_decrease(struct swappy_state *state) {
165 | guint step = state->settings.w <= 10 ? 1 : 5;
166 |
167 | state->settings.w -= step;
168 |
169 | if (state->settings.w < SWAPPY_LINE_SIZE_MIN) {
170 | state->settings.w = SWAPPY_LINE_SIZE_MIN;
171 | }
172 |
173 | update_ui_stroke_size_widget(state);
174 | }
175 |
176 | static void action_stroke_size_reset(struct swappy_state *state) {
177 | state->settings.w = state->config->line_size;
178 |
179 | update_ui_stroke_size_widget(state);
180 | }
181 |
182 | static void action_stroke_size_increase(struct swappy_state *state) {
183 | guint step = state->settings.w >= 10 ? 5 : 1;
184 | state->settings.w += step;
185 |
186 | if (state->settings.w > SWAPPY_LINE_SIZE_MAX) {
187 | state->settings.w = SWAPPY_LINE_SIZE_MAX;
188 | }
189 |
190 | update_ui_stroke_size_widget(state);
191 | }
192 |
193 | static void action_text_size_decrease(struct swappy_state *state) {
194 | guint step = state->settings.t <= 20 ? 1 : 5;
195 | state->settings.t -= step;
196 |
197 | if (state->settings.t < SWAPPY_TEXT_SIZE_MIN) {
198 | state->settings.t = SWAPPY_TEXT_SIZE_MIN;
199 | }
200 |
201 | update_ui_text_size_widget(state);
202 | }
203 | static void action_text_size_reset(struct swappy_state *state) {
204 | state->settings.t = state->config->text_size;
205 | update_ui_text_size_widget(state);
206 | }
207 | static void action_text_size_increase(struct swappy_state *state) {
208 | guint step = state->settings.t >= 20 ? 5 : 1;
209 | state->settings.t += step;
210 |
211 | if (state->settings.t > SWAPPY_TEXT_SIZE_MAX) {
212 | state->settings.t = SWAPPY_TEXT_SIZE_MAX;
213 | }
214 |
215 | update_ui_text_size_widget(state);
216 | }
217 |
218 | static void action_fill_shape_toggle(struct swappy_state *state,
219 | gboolean *toggled) {
220 | // Don't allow changing the state via a shortcut if the button can't be
221 | // clicked.
222 | if (!gtk_widget_get_sensitive(GTK_WIDGET(state->ui->fill_shape))) return;
223 |
224 | gboolean toggle = (toggled == NULL) ? !state->config->fill_shape : *toggled;
225 | state->config->fill_shape = toggle;
226 |
227 | update_ui_fill_shape_toggle_button(state);
228 | }
229 |
230 | static void save_state_to_file_or_folder(struct swappy_state *state,
231 | char *file) {
232 | GdkPixbuf *pixbuf = pixbuf_get_from_state(state);
233 |
234 | if (file == NULL) {
235 | pixbuf_save_state_to_folder(pixbuf, state->config->save_dir,
236 | state->config->save_filename_format);
237 | } else {
238 | pixbuf_save_to_file(pixbuf, file);
239 | }
240 |
241 | g_object_unref(pixbuf);
242 |
243 | if (state->config->early_exit) {
244 | gtk_main_quit();
245 | }
246 | }
247 |
248 | static void maybe_save_output_file(struct swappy_state *state) {
249 | if (state->config->auto_save) {
250 | save_state_to_file_or_folder(state, state->output_file);
251 | }
252 | }
253 |
254 | static void screen_coordinates_to_image_coordinates(struct swappy_state *state,
255 | gdouble screen_x,
256 | gdouble screen_y,
257 | gdouble *image_x,
258 | gdouble *image_y) {
259 | gdouble x, y;
260 |
261 | gint w = gdk_pixbuf_get_width(state->original_image);
262 | gint h = gdk_pixbuf_get_height(state->original_image);
263 |
264 | // Clamp coordinates to original image properties to avoid side effects in
265 | // rendering pipeline
266 | x = CLAMP(screen_x / state->scaling_factor, 0, w);
267 | y = CLAMP(screen_y / state->scaling_factor, 0, h);
268 |
269 | *image_x = x;
270 | *image_y = y;
271 | }
272 |
273 | static void commit_state(struct swappy_state *state) {
274 | paint_commit_temporary(state);
275 | paint_free_list(&state->redo_paints);
276 | render_state(state);
277 | update_ui_undo_redo(state);
278 | }
279 |
280 | void on_destroy(GtkApplication *application, gpointer data) {
281 | struct swappy_state *state = (struct swappy_state *)data;
282 | maybe_save_output_file(state);
283 | }
284 |
285 | void brush_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
286 | switch_mode_to_brush(state);
287 | }
288 |
289 | void text_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
290 | switch_mode_to_text(state);
291 | }
292 |
293 | void rectangle_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
294 | switch_mode_to_rectangle(state);
295 | }
296 |
297 | void ellipse_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
298 | switch_mode_to_ellipse(state);
299 | }
300 |
301 | void arrow_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
302 | switch_mode_to_arrow(state);
303 | }
304 |
305 | void blur_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
306 | switch_mode_to_blur(state);
307 | }
308 |
309 | void save_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
310 | // Commit a potential paint (e.g. text being written)
311 | commit_state(state);
312 | save_state_to_file_or_folder(state, NULL);
313 | }
314 |
315 | void clear_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
316 | action_clear(state);
317 | }
318 |
319 | void copy_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
320 | // Commit a potential paint (e.g. text being written)
321 | commit_state(state);
322 | clipboard_copy_drawing_area_to_selection(state);
323 | }
324 |
325 | void control_modifier_changed(bool pressed, struct swappy_state *state) {
326 | if (state->temp_paint != NULL) {
327 | switch (state->temp_paint->type) {
328 | case SWAPPY_PAINT_MODE_ELLIPSE:
329 | case SWAPPY_PAINT_MODE_RECTANGLE:
330 | paint_update_temporary_shape(
331 | state, state->temp_paint->content.shape.to.x,
332 | state->temp_paint->content.shape.to.y, pressed);
333 | render_state(state);
334 | break;
335 | default:
336 | break;
337 | }
338 | }
339 | }
340 |
341 | void window_keypress_handler(GtkWidget *widget, GdkEventKey *event,
342 | struct swappy_state *state) {
343 | if (state->temp_paint && state->mode == SWAPPY_PAINT_MODE_TEXT) {
344 | paint_update_temporary_text(state, event);
345 | render_state(state);
346 | return;
347 | }
348 | if (event->state & GDK_CONTROL_MASK) {
349 | switch (event->keyval) {
350 | case GDK_KEY_c:
351 | clipboard_copy_drawing_area_to_selection(state);
352 | break;
353 | case GDK_KEY_s:
354 | save_state_to_file_or_folder(state, NULL);
355 | break;
356 | case GDK_KEY_b:
357 | action_toggle_painting_panel(state, NULL);
358 | break;
359 | case GDK_KEY_w:
360 | gtk_main_quit();
361 | break;
362 | case GDK_KEY_z:
363 | action_undo(state);
364 | break;
365 | case GDK_KEY_Z:
366 | case GDK_KEY_y:
367 | action_redo(state);
368 | break;
369 | default:
370 | break;
371 | }
372 | } else {
373 | switch (event->keyval) {
374 | case GDK_KEY_Escape:
375 | case GDK_KEY_q:
376 | maybe_save_output_file(state);
377 | gtk_main_quit();
378 | break;
379 | case GDK_KEY_b:
380 | switch_mode_to_brush(state);
381 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->brush), true);
382 | break;
383 | case GDK_KEY_t:
384 | switch_mode_to_text(state);
385 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->text), true);
386 | break;
387 | case GDK_KEY_r:
388 | switch_mode_to_rectangle(state);
389 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->rectangle),
390 | true);
391 | break;
392 | case GDK_KEY_o:
393 | switch_mode_to_ellipse(state);
394 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->ellipse),
395 | true);
396 | break;
397 | case GDK_KEY_a:
398 | switch_mode_to_arrow(state);
399 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->arrow), true);
400 | break;
401 | case GDK_KEY_d:
402 | switch_mode_to_blur(state);
403 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blur), true);
404 | break;
405 | case GDK_KEY_k:
406 | action_clear(state);
407 | break;
408 | case GDK_KEY_R:
409 | action_update_color_state(state, 1, 0, 0, 1, false);
410 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->red), true);
411 | break;
412 | case GDK_KEY_G:
413 | action_update_color_state(state, 0, 1, 0, 1, false);
414 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->green), true);
415 | break;
416 | case GDK_KEY_B:
417 | action_update_color_state(state, 0, 0, 1, 1, false);
418 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blue), true);
419 | break;
420 | case GDK_KEY_C:
421 | action_set_color_from_custom(state);
422 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->custom),
423 | true);
424 | break;
425 | case GDK_KEY_minus:
426 | action_stroke_size_decrease(state);
427 | break;
428 | case GDK_KEY_equal:
429 | action_stroke_size_reset(state);
430 | break;
431 | case GDK_KEY_plus:
432 | action_stroke_size_increase(state);
433 | break;
434 | case GDK_KEY_Control_L:
435 | control_modifier_changed(true, state);
436 | break;
437 | case GDK_KEY_f:
438 | action_fill_shape_toggle(state, NULL);
439 | break;
440 | default:
441 | break;
442 | }
443 | }
444 | }
445 |
446 | void window_keyrelease_handler(GtkWidget *widget, GdkEventKey *event,
447 | struct swappy_state *state) {
448 | if (event->state & GDK_CONTROL_MASK) {
449 | switch (event->keyval) {
450 | case GDK_KEY_Control_L:
451 | control_modifier_changed(false, state);
452 | break;
453 | default:
454 | break;
455 | }
456 | } else {
457 | switch (event->keyval) {
458 | default:
459 | break;
460 | }
461 | }
462 | }
463 |
464 | gboolean window_delete_handler(GtkWidget *widget, GdkEvent *event,
465 | struct swappy_state *state) {
466 | gtk_main_quit();
467 | return FALSE;
468 | }
469 |
470 | void pane_toggled_handler(GtkWidget *widget, struct swappy_state *state) {
471 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget);
472 | gboolean toggled = gtk_toggle_button_get_active(button);
473 | action_toggle_painting_panel(state, &toggled);
474 | }
475 |
476 | void undo_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
477 | action_undo(state);
478 | }
479 |
480 | void redo_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
481 | action_redo(state);
482 | }
483 |
484 | gboolean draw_area_handler(GtkWidget *widget, cairo_t *cr,
485 | struct swappy_state *state) {
486 | GtkAllocation *alloc = g_new(GtkAllocation, 1);
487 | gtk_widget_get_allocation(widget, alloc);
488 |
489 | GdkPixbuf *image = state->original_image;
490 | gint image_width = gdk_pixbuf_get_width(image);
491 | gint image_height = gdk_pixbuf_get_height(image);
492 | double scale_x = (double)alloc->width / image_width;
493 | double scale_y = (double)alloc->height / image_height;
494 |
495 | cairo_scale(cr, scale_x, scale_y);
496 | cairo_set_source_surface(cr, state->rendering_surface, 0, 0);
497 | cairo_paint(cr);
498 |
499 | return FALSE;
500 | }
501 |
502 | gboolean draw_area_configure_handler(GtkWidget *widget,
503 | GdkEventConfigure *event,
504 | struct swappy_state *state) {
505 | g_debug("received configure_event callback");
506 |
507 | pixbuf_scale_surface_from_widget(state, widget);
508 |
509 | render_state(state);
510 |
511 | return TRUE;
512 | }
513 |
514 | void draw_area_button_press_handler(GtkWidget *widget, GdkEventButton *event,
515 | struct swappy_state *state) {
516 | gdouble x, y;
517 |
518 | screen_coordinates_to_image_coordinates(state, event->x, event->y, &x, &y);
519 |
520 | if (event->button == 1) {
521 | switch (state->mode) {
522 | case SWAPPY_PAINT_MODE_BLUR:
523 | case SWAPPY_PAINT_MODE_BRUSH:
524 | case SWAPPY_PAINT_MODE_RECTANGLE:
525 | case SWAPPY_PAINT_MODE_ELLIPSE:
526 | case SWAPPY_PAINT_MODE_ARROW:
527 | case SWAPPY_PAINT_MODE_TEXT:
528 | paint_add_temporary(state, x, y, state->mode);
529 | render_state(state);
530 | update_ui_undo_redo(state);
531 | break;
532 | default:
533 | return;
534 | }
535 | }
536 | }
537 | void draw_area_motion_notify_handler(GtkWidget *widget, GdkEventMotion *event,
538 | struct swappy_state *state) {
539 | gdouble x, y;
540 |
541 | screen_coordinates_to_image_coordinates(state, event->x, event->y, &x, &y);
542 |
543 | GdkDisplay *display = gdk_display_get_default();
544 | GdkWindow *window = event->window;
545 | GdkCursor *crosshair = gdk_cursor_new_for_display(display, GDK_CROSSHAIR);
546 | gdk_window_set_cursor(window, crosshair);
547 |
548 | gboolean is_button1_pressed = event->state & GDK_BUTTON1_MASK;
549 | gboolean is_control_pressed = event->state & GDK_CONTROL_MASK;
550 |
551 | switch (state->mode) {
552 | case SWAPPY_PAINT_MODE_BLUR:
553 | case SWAPPY_PAINT_MODE_BRUSH:
554 | case SWAPPY_PAINT_MODE_RECTANGLE:
555 | case SWAPPY_PAINT_MODE_ELLIPSE:
556 | case SWAPPY_PAINT_MODE_ARROW:
557 | if (is_button1_pressed) {
558 | paint_update_temporary_shape(state, x, y, is_control_pressed);
559 | render_state(state);
560 | }
561 | break;
562 | case SWAPPY_PAINT_MODE_TEXT:
563 | if (is_button1_pressed) {
564 | paint_update_temporary_text_clip(state, x, y);
565 | render_state(state);
566 | }
567 | break;
568 | default:
569 | return;
570 | }
571 | g_object_unref(crosshair);
572 | }
573 | void draw_area_button_release_handler(GtkWidget *widget, GdkEventButton *event,
574 | struct swappy_state *state) {
575 | if (!(event->state & GDK_BUTTON1_MASK)) {
576 | return;
577 | }
578 |
579 | switch (state->mode) {
580 | case SWAPPY_PAINT_MODE_BLUR:
581 | case SWAPPY_PAINT_MODE_BRUSH:
582 | case SWAPPY_PAINT_MODE_RECTANGLE:
583 | case SWAPPY_PAINT_MODE_ELLIPSE:
584 | case SWAPPY_PAINT_MODE_ARROW:
585 | commit_state(state);
586 | break;
587 | case SWAPPY_PAINT_MODE_TEXT:
588 | if (state->temp_paint && !state->temp_paint->can_draw) {
589 | paint_free(state->temp_paint);
590 | state->temp_paint = NULL;
591 | }
592 | break;
593 | default:
594 | return;
595 | }
596 | }
597 |
598 | void color_red_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
599 | action_update_color_state(state, 1, 0, 0, 1, false);
600 | }
601 |
602 | void color_green_clicked_handler(GtkWidget *widget,
603 | struct swappy_state *state) {
604 | action_update_color_state(state, 0, 1, 0, 1, false);
605 | }
606 |
607 | void color_blue_clicked_handler(GtkWidget *widget, struct swappy_state *state) {
608 | action_update_color_state(state, 0, 0, 1, 1, false);
609 | }
610 |
611 | void color_custom_clicked_handler(GtkWidget *widget,
612 | struct swappy_state *state) {
613 | action_set_color_from_custom(state);
614 | }
615 |
616 | void color_custom_color_set_handler(GtkWidget *widget,
617 | struct swappy_state *state) {
618 | action_set_color_from_custom(state);
619 | }
620 |
621 | void stroke_size_decrease_handler(GtkWidget *widget,
622 | struct swappy_state *state) {
623 | action_stroke_size_decrease(state);
624 | }
625 |
626 | void stroke_size_reset_handler(GtkWidget *widget, struct swappy_state *state) {
627 | action_stroke_size_reset(state);
628 | }
629 | void stroke_size_increase_handler(GtkWidget *widget,
630 | struct swappy_state *state) {
631 | action_stroke_size_increase(state);
632 | }
633 |
634 | void text_size_decrease_handler(GtkWidget *widget, struct swappy_state *state) {
635 | action_text_size_decrease(state);
636 | }
637 | void text_size_reset_handler(GtkWidget *widget, struct swappy_state *state) {
638 | action_text_size_reset(state);
639 | }
640 | void text_size_increase_handler(GtkWidget *widget, struct swappy_state *state) {
641 | action_text_size_increase(state);
642 | }
643 |
644 | void fill_shape_toggled_handler(GtkWidget *widget, struct swappy_state *state) {
645 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget);
646 | gboolean toggled = gtk_toggle_button_get_active(button);
647 | action_fill_shape_toggle(state, &toggled);
648 | }
649 |
650 | static void compute_window_size_and_scaling_factor(struct swappy_state *state) {
651 | GdkRectangle workarea = {0};
652 | GdkDisplay *display = gdk_display_get_default();
653 | GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(state->ui->window));
654 | GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window);
655 | gdk_monitor_get_workarea(monitor, &workarea);
656 |
657 | g_assert(workarea.width > 0);
658 | g_assert(workarea.height > 0);
659 |
660 | if (state->window) {
661 | g_free(state->window);
662 | state->window = NULL;
663 | }
664 |
665 | state->window = g_new(struct swappy_box, 1);
666 | state->window->x = workarea.x;
667 | state->window->y = workarea.y;
668 |
669 | double threshold = 0.75;
670 | double scaling_factor = 1.0;
671 |
672 | int image_width = gdk_pixbuf_get_width(state->original_image);
673 | int image_height = gdk_pixbuf_get_height(state->original_image);
674 |
675 | int max_width = workarea.width * threshold;
676 | int max_height = workarea.height * threshold;
677 |
678 | g_info("size of image: %ux%u", image_width, image_height);
679 | g_info("size of monitor at window: %ux%u", workarea.width, workarea.height);
680 | g_info("maxium size allowed for window: %ux%u", max_width, max_height);
681 |
682 | int scaled_width = image_width;
683 | int scaled_height = image_height;
684 |
685 | double scaling_factor_width = (double)max_width / image_width;
686 | double scaling_factor_height = (double)max_height / image_height;
687 |
688 | if (scaling_factor_height < 1.0 || scaling_factor_width < 1.0) {
689 | scaling_factor = MIN(scaling_factor_width, scaling_factor_height);
690 | scaled_width = image_width * scaling_factor;
691 | scaled_height = image_height * scaling_factor;
692 | g_info("rendering area will be scaled by a factor of: %.2lf",
693 | scaling_factor);
694 | }
695 |
696 | state->scaling_factor = scaling_factor;
697 | state->window->width = scaled_width;
698 | state->window->height = scaled_height;
699 |
700 | g_info("size of window to render: %ux%u", state->window->width,
701 | state->window->height);
702 | }
703 |
704 | static void apply_css(GtkWidget *widget, GtkStyleProvider *provider) {
705 | gtk_style_context_add_provider(gtk_widget_get_style_context(widget), provider,
706 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
707 | if (GTK_IS_CONTAINER(widget)) {
708 | gtk_container_forall(GTK_CONTAINER(widget), (GtkCallback)apply_css,
709 | provider);
710 | }
711 | }
712 |
713 | static bool load_css(struct swappy_state *state) {
714 | GtkCssProvider *provider = gtk_css_provider_new();
715 | gtk_css_provider_load_from_resource(provider,
716 | "/me/jtheoof/swappy/style/swappy.css");
717 | apply_css(GTK_WIDGET(state->ui->window), GTK_STYLE_PROVIDER(provider));
718 | g_object_unref(provider);
719 | return true;
720 | }
721 |
722 | static bool load_layout(struct swappy_state *state) {
723 | GError *error = NULL;
724 | // init color
725 | GdkRGBA color;
726 |
727 | /* Construct a GtkBuilder instance and load our UI description */
728 | GtkBuilder *builder = gtk_builder_new();
729 |
730 | // Set translation domain for the application based on `src/po/meson.build`
731 | gtk_builder_set_translation_domain(builder, GETTEXT_PACKAGE);
732 |
733 | if (gtk_builder_add_from_resource(builder, "/me/jtheoof/swappy/swappy.glade",
734 | &error) == 0) {
735 | g_printerr("Error loading file: %s", error->message);
736 | g_clear_error(&error);
737 | return false;
738 | }
739 |
740 | gtk_builder_connect_signals(builder, state);
741 |
742 | GtkWindow *window =
743 | GTK_WINDOW(gtk_builder_get_object(builder, "paint-window"));
744 |
745 | g_signal_connect(window, "destroy", G_CALLBACK(on_destroy), state);
746 |
747 | state->ui->panel_toggle_button =
748 | GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "btn-toggle-panel"));
749 |
750 | state->ui->undo = GTK_BUTTON(gtk_builder_get_object(builder, "undo-button"));
751 | state->ui->redo = GTK_BUTTON(gtk_builder_get_object(builder, "redo-button"));
752 |
753 | GtkWidget *area =
754 | GTK_WIDGET(gtk_builder_get_object(builder, "painting-area"));
755 |
756 | state->ui->painting_box =
757 | GTK_BOX(gtk_builder_get_object(builder, "painting-box"));
758 | GtkRadioButton *brush =
759 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "brush"));
760 | GtkRadioButton *text =
761 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "text"));
762 | GtkRadioButton *rectangle =
763 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "rectangle"));
764 | GtkRadioButton *ellipse =
765 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "ellipse"));
766 | GtkRadioButton *arrow =
767 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "arrow"));
768 | GtkRadioButton *blur =
769 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "blur"));
770 |
771 | state->ui->red =
772 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-red-button"));
773 | state->ui->green =
774 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-green-button"));
775 | state->ui->blue =
776 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-blue-button"));
777 | state->ui->custom =
778 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-custom-button"));
779 | state->ui->color =
780 | GTK_COLOR_BUTTON(gtk_builder_get_object(builder, "custom-color-button"));
781 |
782 | state->ui->line_size =
783 | GTK_BUTTON(gtk_builder_get_object(builder, "stroke-size-button"));
784 | state->ui->text_size =
785 | GTK_BUTTON(gtk_builder_get_object(builder, "text-size-button"));
786 |
787 | state->ui->fill_shape = GTK_TOGGLE_BUTTON(
788 | gtk_builder_get_object(builder, "fill-shape-toggle-button"));
789 |
790 | gdk_rgba_parse(&color, state->config->custom_color);
791 | gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(state->ui->color), &color);
792 |
793 | state->ui->brush = brush;
794 | state->ui->text = text;
795 | state->ui->rectangle = rectangle;
796 | state->ui->ellipse = ellipse;
797 | state->ui->arrow = arrow;
798 | state->ui->blur = blur;
799 | state->ui->area = area;
800 | state->ui->window = window;
801 |
802 | compute_window_size_and_scaling_factor(state);
803 | gtk_widget_set_size_request(area, state->window->width,
804 | state->window->height);
805 | action_toggle_painting_panel(state, &state->config->show_panel);
806 |
807 | g_object_unref(G_OBJECT(builder));
808 |
809 | return true;
810 | }
811 |
812 | static void set_paint_mode(struct swappy_state *state) {
813 | switch (state->mode) {
814 | case SWAPPY_PAINT_MODE_BRUSH:
815 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->brush), true);
816 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
817 | break;
818 | case SWAPPY_PAINT_MODE_TEXT:
819 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->text), true);
820 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
821 | break;
822 | case SWAPPY_PAINT_MODE_RECTANGLE:
823 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->rectangle),
824 | true);
825 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true);
826 | break;
827 | case SWAPPY_PAINT_MODE_ELLIPSE:
828 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->ellipse), true);
829 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true);
830 | break;
831 | case SWAPPY_PAINT_MODE_ARROW:
832 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->arrow), true);
833 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
834 | break;
835 | case SWAPPY_PAINT_MODE_BLUR:
836 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blur), true);
837 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false);
838 | break;
839 | default:
840 | break;
841 | }
842 | }
843 |
844 | static bool init_gtk_window(struct swappy_state *state) {
845 | if (!state->original_image) {
846 | g_critical("original image not loaded");
847 | return false;
848 | }
849 |
850 | if (!load_layout(state)) {
851 | return false;
852 | }
853 |
854 | if (!load_css(state)) {
855 | return false;
856 | }
857 |
858 | set_paint_mode(state);
859 |
860 | update_ui_stroke_size_widget(state);
861 | update_ui_text_size_widget(state);
862 | update_ui_undo_redo(state);
863 | update_ui_panel_toggle_button(state);
864 | update_ui_fill_shape_toggle_button(state);
865 |
866 | return true;
867 | }
868 |
869 | static gboolean has_option_file(struct swappy_state *state) {
870 | return (state->file_str != NULL);
871 | }
872 |
873 | static gboolean is_file_from_stdin(const char *file) {
874 | return (strcmp(file, "-") == 0);
875 | }
876 |
877 | static void init_settings(struct swappy_state *state) {
878 | state->settings.r = 1;
879 | state->settings.g = 0;
880 | state->settings.b = 0;
881 | state->settings.a = 1;
882 | state->settings.w = state->config->line_size;
883 | state->settings.t = state->config->text_size;
884 | state->mode = state->config->paint_mode;
885 | }
886 |
887 | static gint command_line_handler(GtkApplication *app,
888 | GApplicationCommandLine *cmdline,
889 | struct swappy_state *state) {
890 | config_load(state);
891 | init_settings(state);
892 |
893 | if (has_option_file(state)) {
894 | if (is_file_from_stdin(state->file_str)) {
895 | char *temp_file_str = file_dump_stdin_into_a_temp_file();
896 | state->temp_file_str = temp_file_str;
897 | }
898 |
899 | if (!pixbuf_init_from_file(state)) {
900 | return EXIT_FAILURE;
901 | }
902 | }
903 |
904 | if (!init_gtk_window(state)) {
905 | return EXIT_FAILURE;
906 | }
907 |
908 | return EXIT_SUCCESS;
909 | }
910 |
911 | // Print version and quit
912 | gboolean callback_on_flag(const gchar *option_name, const gchar *value,
913 | gpointer data, GError **error) {
914 | if (!strcmp(option_name, "-v") || !strcmp(option_name, "--version")) {
915 | printf("swappy version %s\n", SWAPPY_VERSION);
916 | exit(0);
917 | }
918 | return TRUE;
919 | }
920 |
921 | bool application_init(struct swappy_state *state) {
922 | // Callback function for flags
923 | gboolean (*GOptionArgFunc)(const gchar *option_name, const gchar *value,
924 | gpointer data, GError **error);
925 | GOptionArgFunc = &callback_on_flag;
926 |
927 | const GOptionEntry cli_options[] = {
928 | {
929 | .long_name = "file",
930 | .short_name = 'f',
931 | .arg = G_OPTION_ARG_STRING,
932 | .arg_data = &state->file_str,
933 | .description = "Load a file at a specific path",
934 | },
935 | {
936 | .long_name = "output-file",
937 | .short_name = 'o',
938 | .arg = G_OPTION_ARG_STRING,
939 | .arg_data = &state->output_file,
940 | .description = "Print the final surface to the given file when "
941 | "exiting, use - to print to stdout",
942 | },
943 | {
944 | .long_name = "version",
945 | .short_name = 'v',
946 | .flags = G_OPTION_FLAG_NO_ARG,
947 | .arg = G_OPTION_ARG_CALLBACK,
948 | .arg_data = GOptionArgFunc,
949 | .description = "Print version and quit",
950 | },
951 | {NULL}};
952 |
953 | state->app = gtk_application_new("me.jtheoof.swappy",
954 | G_APPLICATION_HANDLES_COMMAND_LINE);
955 |
956 | if (state->app == NULL) {
957 | g_critical("cannot create gtk application");
958 | return false;
959 | }
960 |
961 | g_application_add_main_option_entries(G_APPLICATION(state->app), cli_options);
962 |
963 | state->ui = g_new(struct swappy_state_ui, 1);
964 | state->ui->panel_toggled = false;
965 |
966 | g_signal_connect(state->app, "command-line", G_CALLBACK(command_line_handler),
967 | state);
968 |
969 | return true;
970 | }
971 |
972 | int application_run(struct swappy_state *state) {
973 | return g_application_run(G_APPLICATION(state->app), state->argc, state->argv);
974 | }
975 |
--------------------------------------------------------------------------------
/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("paint_mode: %d", config->paint_mode);
23 | g_info("early_exit: %d", config->early_exit);
24 | g_info("fill_shape: %d", config->fill_shape);
25 | g_info("auto_save: %d", config->auto_save);
26 | g_info("custom_color: %s", config->custom_color);
27 | }
28 |
29 | static char *get_default_save_dir() {
30 | static const char *storage_paths[] = {
31 | "$XDG_DESKTOP_DIR",
32 | "$XDG_CONFIG_HOME/Desktop",
33 | "$HOME/Desktop",
34 | "$HOME",
35 | };
36 |
37 | for (size_t i = 0; i < sizeof(storage_paths) / sizeof(char *); ++i) {
38 | wordexp_t p;
39 | if (wordexp(storage_paths[i], &p, 0) == 0) {
40 | char *path = g_strdup(p.we_wordv[0]);
41 | wordfree(&p);
42 | if (path && folder_exists(path)) {
43 | return path;
44 | }
45 | g_free(path);
46 | }
47 | }
48 |
49 | return NULL;
50 | }
51 |
52 | static char *get_config_file() {
53 | static const char *storage_paths[] = {
54 | "$XDG_CONFIG_HOME/swappy/config",
55 | "$HOME/.config/swappy/config",
56 | };
57 |
58 | for (size_t i = 0; i < sizeof(storage_paths) / sizeof(char *); ++i) {
59 | wordexp_t p;
60 | if (wordexp(storage_paths[i], &p, 0) == 0) {
61 | char *path = g_strdup(p.we_wordv[0]);
62 | wordfree(&p);
63 | if (path && file_exists(path)) {
64 | return path;
65 | }
66 | g_free(path);
67 | }
68 | }
69 |
70 | return NULL;
71 | }
72 |
73 | static void load_config_from_file(struct swappy_config *config,
74 | const char *file) {
75 | GKeyFile *gkf;
76 | const gchar *group = "Default";
77 | gchar *save_dir = NULL;
78 | gchar *save_filename_format = NULL;
79 | gboolean show_panel;
80 | gchar *save_dir_expanded = NULL;
81 | guint64 line_size, text_size;
82 | gchar *text_font = NULL;
83 | gchar *paint_mode = NULL;
84 | gboolean early_exit;
85 | gboolean fill_shape;
86 | gboolean auto_save;
87 | gchar *custom_color = NULL;
88 | GError *error = NULL;
89 |
90 | if (file == NULL) {
91 | return;
92 | }
93 |
94 | gkf = g_key_file_new();
95 |
96 | if (!g_key_file_load_from_file(gkf, file, G_KEY_FILE_NONE, NULL)) {
97 | g_warning("could not read config file %s", file);
98 | g_key_file_free(gkf);
99 | return;
100 | }
101 |
102 | save_dir = g_key_file_get_string(gkf, group, "save_dir", &error);
103 |
104 | if (error == NULL) {
105 | wordexp_t p;
106 | if (wordexp(save_dir, &p, 0) == 0) {
107 | save_dir_expanded = g_strdup(p.we_wordv[0]);
108 | wordfree(&p);
109 | if (!save_dir_expanded || !folder_exists(save_dir_expanded)) {
110 | g_info("save_dir: attempting to create non-existent directory '%s'",
111 | save_dir_expanded);
112 | if (g_mkdir_with_parents(save_dir_expanded, 0755)) {
113 | g_warning("save_dir: failed to create '%s'", save_dir_expanded);
114 | }
115 | }
116 |
117 | g_free(save_dir);
118 | g_free(config->save_dir);
119 | config->save_dir = save_dir_expanded;
120 | }
121 | } else {
122 | g_info("save_dir is missing in %s (%s)", file, error->message);
123 | g_error_free(error);
124 | error = NULL;
125 | }
126 |
127 | save_filename_format =
128 | g_key_file_get_string(gkf, group, "save_filename_format", &error);
129 |
130 | if (error == NULL) {
131 | config->save_filename_format = save_filename_format;
132 | } else {
133 | g_info("save_filename_format is missing in %s (%s)", file, error->message);
134 | g_error_free(error);
135 | error = NULL;
136 | }
137 |
138 | line_size = g_key_file_get_uint64(gkf, group, "line_size", &error);
139 |
140 | if (error == NULL) {
141 | if (line_size >= SWAPPY_LINE_SIZE_MIN &&
142 | line_size <= SWAPPY_LINE_SIZE_MAX) {
143 | config->line_size = line_size;
144 | } else {
145 | g_warning("line_size is not a valid value: %" PRIu64
146 | " - see man page for details",
147 | line_size);
148 | }
149 | } else {
150 | g_info("line_size is missing in %s (%s)", file, error->message);
151 | g_error_free(error);
152 | error = NULL;
153 | }
154 |
155 | text_size = g_key_file_get_uint64(gkf, group, "text_size", &error);
156 |
157 | if (error == NULL) {
158 | if (text_size >= SWAPPY_TEXT_SIZE_MIN &&
159 | text_size <= SWAPPY_TEXT_SIZE_MAX) {
160 | config->text_size = text_size;
161 | } else {
162 | g_warning("text_size is not a valid value: %" PRIu64
163 | " - see man page for details",
164 | text_size);
165 | }
166 | } else {
167 | g_info("text_size is missing in %s (%s)", file, error->message);
168 | g_error_free(error);
169 | error = NULL;
170 | }
171 |
172 | text_font = g_key_file_get_string(gkf, group, "text_font", &error);
173 |
174 | if (error == NULL) {
175 | g_free(config->text_font);
176 | config->text_font = text_font;
177 | } else {
178 | g_info("text_font is missing in %s (%s)", file, error->message);
179 | g_error_free(error);
180 | error = NULL;
181 | }
182 |
183 | show_panel = g_key_file_get_boolean(gkf, group, "show_panel", &error);
184 |
185 | if (error == NULL) {
186 | config->show_panel = show_panel;
187 | } else {
188 | g_info("show_panel is missing in %s (%s)", file, error->message);
189 | g_error_free(error);
190 | error = NULL;
191 | }
192 |
193 | early_exit = g_key_file_get_boolean(gkf, group, "early_exit", &error);
194 |
195 | if (error == NULL) {
196 | config->early_exit = early_exit;
197 | } else {
198 | g_info("early_exit is missing in %s (%s)", file, error->message);
199 | g_error_free(error);
200 | error = NULL;
201 | }
202 |
203 | paint_mode = g_key_file_get_string(gkf, group, "paint_mode", &error);
204 |
205 | if (error == NULL) {
206 | if (g_ascii_strcasecmp(paint_mode, "brush") == 0) {
207 | config->paint_mode = SWAPPY_PAINT_MODE_BRUSH;
208 | } else if (g_ascii_strcasecmp(paint_mode, "text") == 0) {
209 | config->paint_mode = SWAPPY_PAINT_MODE_TEXT;
210 | } else if (g_ascii_strcasecmp(paint_mode, "rectangle") == 0) {
211 | config->paint_mode = SWAPPY_PAINT_MODE_RECTANGLE;
212 | } else if (g_ascii_strcasecmp(paint_mode, "ellipse") == 0) {
213 | config->paint_mode = SWAPPY_PAINT_MODE_ELLIPSE;
214 | } else if (g_ascii_strcasecmp(paint_mode, "arrow") == 0) {
215 | config->paint_mode = SWAPPY_PAINT_MODE_ARROW;
216 | } else if (g_ascii_strcasecmp(paint_mode, "blur") == 0) {
217 | config->paint_mode = SWAPPY_PAINT_MODE_BLUR;
218 | } else {
219 | g_warning(
220 | "paint_mode is not a valid value: %s - see man page for details",
221 | paint_mode);
222 | }
223 | } else {
224 | g_info("paint_mode is missing in %s (%s)", file, error->message);
225 | g_error_free(error);
226 | error = NULL;
227 | }
228 |
229 | fill_shape = g_key_file_get_boolean(gkf, group, "fill_shape", &error);
230 |
231 | if (error == NULL) {
232 | config->fill_shape = fill_shape;
233 | } else {
234 | g_info("fill_shape is missing in %s (%s)", file, error->message);
235 | g_error_free(error);
236 | error = NULL;
237 | }
238 |
239 | auto_save = g_key_file_get_boolean(gkf, group, "auto_save", &error);
240 |
241 | if (error == NULL) {
242 | config->auto_save = auto_save;
243 | } else {
244 | g_info("auto_save is missing in %s (%s)", file, error->message);
245 | g_error_free(error);
246 | error = NULL;
247 | }
248 |
249 | custom_color = g_key_file_get_string(gkf, group, "custom_color", &error);
250 |
251 | if (error == NULL) {
252 | config->custom_color = custom_color;
253 | } else {
254 | g_info("custom_color is missing in %s (%s)", file, error->message);
255 | g_error_free(error);
256 | error = NULL;
257 | }
258 |
259 | g_key_file_free(gkf);
260 | }
261 |
262 | static void load_default_config(struct swappy_config *config) {
263 | if (config == NULL) {
264 | return;
265 | }
266 |
267 | config->save_dir = get_default_save_dir();
268 | config->save_filename_format = g_strdup(CONFIG_SAVE_FILENAME_FORMAT_DEFAULT);
269 | config->line_size = CONFIG_LINE_SIZE_DEFAULT;
270 | config->text_font = g_strdup(CONFIG_TEXT_FONT_DEFAULT);
271 | config->text_size = CONFIG_TEXT_SIZE_DEFAULT;
272 | config->show_panel = CONFIG_SHOW_PANEL_DEFAULT;
273 | config->paint_mode = CONFIG_PAINT_MODE_DEFAULT;
274 | config->early_exit = CONFIG_EARLY_EXIT_DEFAULT;
275 | config->fill_shape = CONFIG_FILL_SHAPE_DEFAULT;
276 | config->auto_save = CONFIG_AUTO_SAVE_DEFAULT;
277 | config->custom_color = g_strdup(CONFIG_CUSTOM_COLOR_DEFAULT);
278 | }
279 |
280 | void config_load(struct swappy_state *state) {
281 | struct swappy_config *config = g_new(struct swappy_config, 1);
282 |
283 | load_default_config(config);
284 |
285 | char *file = get_config_file();
286 | if (file) {
287 | load_config_from_file(config, file);
288 | } else {
289 | g_info("could not find swappy config file, using defaults");
290 | }
291 |
292 | config->config_file = file;
293 | state->config = config;
294 |
295 | print_config(state->config);
296 | }
297 |
298 | void config_free(struct swappy_state *state) {
299 | if (state->config) {
300 | g_free(state->config->config_file);
301 | g_free(state->config->save_dir);
302 | g_free(state->config->save_filename_format);
303 | g_free(state->config->text_font);
304 | g_free(state->config->custom_color);
305 | g_free(state->config);
306 | state->config = NULL;
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/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 |
5 | #include "util.h"
6 |
7 | static void cursor_move_backward(struct swappy_paint_text *text) {
8 | if (text->cursor > 0) {
9 | text->cursor--;
10 | }
11 | }
12 |
13 | static void cursor_move_forward(struct swappy_paint_text *text) {
14 | if (text->cursor < g_utf8_strlen(text->text, -1)) {
15 | text->cursor++;
16 | }
17 | }
18 |
19 | void paint_free(gpointer data) {
20 | struct swappy_paint *paint = (struct swappy_paint *)data;
21 |
22 | if (paint == NULL) {
23 | return;
24 | }
25 |
26 | switch (paint->type) {
27 | case SWAPPY_PAINT_MODE_BLUR:
28 | if (paint->content.blur.surface) {
29 | cairo_surface_destroy(paint->content.blur.surface);
30 | }
31 | break;
32 | case SWAPPY_PAINT_MODE_BRUSH:
33 | g_list_free_full(paint->content.brush.points, g_free);
34 | break;
35 | case SWAPPY_PAINT_MODE_TEXT:
36 | g_free(paint->content.text.text);
37 | g_free(paint->content.text.font);
38 | break;
39 | default:
40 | break;
41 | }
42 | g_free(paint);
43 | }
44 |
45 | void paint_free_list(GList **list) {
46 | if (*list) {
47 | g_list_free_full(*list, paint_free);
48 | *list = NULL;
49 | }
50 | }
51 |
52 | void paint_free_all(struct swappy_state *state) {
53 | paint_free_list(&state->paints);
54 | paint_free_list(&state->redo_paints);
55 | paint_free(state->temp_paint);
56 | state->temp_paint = NULL;
57 | }
58 |
59 | void paint_add_temporary(struct swappy_state *state, double x, double y,
60 | enum swappy_paint_type type) {
61 | struct swappy_paint *paint = g_new(struct swappy_paint, 1);
62 | struct swappy_point *point;
63 |
64 | double r = state->settings.r;
65 | double g = state->settings.g;
66 | double b = state->settings.b;
67 | double a = state->settings.a;
68 | double w = state->settings.w;
69 | double t = state->settings.t;
70 |
71 | paint->type = type;
72 | paint->is_committed = false;
73 |
74 | g_debug("adding temporary paint at: %.2lfx%.2lf", x, y);
75 |
76 | if (state->temp_paint) {
77 | if (type == SWAPPY_PAINT_MODE_TEXT) {
78 | paint_commit_temporary(state);
79 | } else {
80 | paint_free(state->temp_paint);
81 | state->temp_paint = NULL;
82 | }
83 | }
84 |
85 | switch (type) {
86 | case SWAPPY_PAINT_MODE_BLUR:
87 | paint->can_draw = false;
88 |
89 | paint->content.blur.from.x = x;
90 | paint->content.blur.from.y = y;
91 | paint->content.blur.surface = NULL;
92 | break;
93 | case SWAPPY_PAINT_MODE_BRUSH:
94 | paint->can_draw = true;
95 |
96 | paint->content.brush.r = r;
97 | paint->content.brush.g = g;
98 | paint->content.brush.b = b;
99 | paint->content.brush.a = a;
100 | paint->content.brush.w = w;
101 |
102 | point = g_new(struct swappy_point, 1);
103 | point->x = x;
104 | point->y = y;
105 |
106 | paint->content.brush.points = g_list_prepend(NULL, point);
107 | break;
108 | case SWAPPY_PAINT_MODE_RECTANGLE:
109 | case SWAPPY_PAINT_MODE_ELLIPSE:
110 | case SWAPPY_PAINT_MODE_ARROW:
111 | paint->can_draw = false; // need `to` vector
112 |
113 | paint->content.shape.from.x = x;
114 | paint->content.shape.from.y = y;
115 | paint->content.shape.r = r;
116 | paint->content.shape.g = g;
117 | paint->content.shape.b = b;
118 | paint->content.shape.a = a;
119 | paint->content.shape.w = w;
120 | paint->content.shape.type = type;
121 | if (state->config->fill_shape)
122 | paint->content.shape.operation = SWAPPY_PAINT_SHAPE_OPERATION_FILL;
123 | else
124 | paint->content.shape.operation = SWAPPY_PAINT_SHAPE_OPERATION_STROKE;
125 | break;
126 | case SWAPPY_PAINT_MODE_TEXT:
127 | paint->can_draw = false;
128 |
129 | paint->content.text.from.x = x;
130 | paint->content.text.from.y = y;
131 | paint->content.text.r = r;
132 | paint->content.text.g = g;
133 | paint->content.text.b = b;
134 | paint->content.text.a = a;
135 | paint->content.text.s = t;
136 | paint->content.text.font = g_strdup(state->config->text_font);
137 | paint->content.text.cursor = 0;
138 | paint->content.text.mode = SWAPPY_TEXT_MODE_EDIT;
139 | paint->content.text.text = g_new(gchar, 1);
140 | paint->content.text.text[0] = '\0';
141 | break;
142 |
143 | default:
144 | g_info("unable to add temporary paint: %d", type);
145 | break;
146 | }
147 |
148 | state->temp_paint = paint;
149 | }
150 |
151 | void paint_update_temporary_shape(struct swappy_state *state, double x,
152 | double y, gboolean is_control_pressed) {
153 | struct swappy_paint *paint = state->temp_paint;
154 | struct swappy_point *point;
155 | GList *points;
156 |
157 | if (!paint) {
158 | return;
159 | }
160 |
161 | switch (paint->type) {
162 | case SWAPPY_PAINT_MODE_BLUR:
163 | paint->can_draw = true;
164 | paint->content.blur.to.x = x;
165 | paint->content.blur.to.y = y;
166 | break;
167 | case SWAPPY_PAINT_MODE_BRUSH:
168 | points = paint->content.brush.points;
169 | point = g_new(struct swappy_point, 1);
170 | point->x = x;
171 | point->y = y;
172 |
173 | paint->content.brush.points = g_list_prepend(points, point);
174 | break;
175 | case SWAPPY_PAINT_MODE_RECTANGLE:
176 | case SWAPPY_PAINT_MODE_ELLIPSE:
177 | paint->can_draw = true; // all set
178 |
179 | paint->content.shape.should_center_at_from = is_control_pressed;
180 | paint->content.shape.to.x = x;
181 | paint->content.shape.to.y = y;
182 | break;
183 | case SWAPPY_PAINT_MODE_ARROW:
184 | paint->can_draw = true; // all set
185 |
186 | paint->content.shape.to.x = x;
187 | paint->content.shape.to.y = y;
188 | break;
189 | default:
190 | g_info("unable to update temporary paint when type is: %d", paint->type);
191 | break;
192 | }
193 | }
194 |
195 | void paint_update_temporary_text(struct swappy_state *state,
196 | GdkEventKey *event) {
197 | struct swappy_paint *paint = state->temp_paint;
198 | struct swappy_paint_text *text;
199 | char *new_text;
200 | char buffer[32];
201 | guint32 unicode;
202 |
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 |
210 | switch (event->keyval) {
211 | case GDK_KEY_Escape:
212 | paint_commit_temporary(state);
213 | break;
214 | case GDK_KEY_BackSpace:
215 | if (g_utf8_strlen(text->text, -1) > 0) {
216 | new_text = string_remove_at(text->text, text->cursor - 1);
217 | g_free(text->text);
218 | text->text = new_text;
219 | cursor_move_backward(text);
220 | }
221 | break;
222 | case GDK_KEY_Delete:
223 | if (g_utf8_strlen(text->text, -1) > 0) {
224 | new_text = string_remove_at(text->text, text->cursor);
225 | g_free(text->text);
226 | text->text = new_text;
227 | }
228 | break;
229 | case GDK_KEY_Left:
230 | cursor_move_backward(text);
231 | break;
232 | case GDK_KEY_Right:
233 | cursor_move_forward(text);
234 | break;
235 | default:
236 | unicode = gdk_keyval_to_unicode(event->keyval);
237 | if (unicode != 0) {
238 | int ll = g_unichar_to_utf8(unicode, buffer);
239 | buffer[ll] = '\0';
240 | char *new_text =
241 | string_insert_chars_at(text->text, buffer, text->cursor);
242 | g_free(text->text);
243 | text->text = new_text;
244 | text->cursor++;
245 | }
246 | break;
247 | }
248 | }
249 |
250 | void paint_update_temporary_text_clip(struct swappy_state *state, gdouble x,
251 | gdouble y) {
252 | struct swappy_paint *paint = state->temp_paint;
253 |
254 | if (!paint) {
255 | return;
256 | }
257 |
258 | g_assert(paint->type == SWAPPY_PAINT_MODE_TEXT);
259 |
260 | paint->can_draw = true;
261 | paint->content.text.to.x = x;
262 | paint->content.text.to.y = y;
263 | }
264 |
265 | void paint_commit_temporary(struct swappy_state *state) {
266 | struct swappy_paint *paint = state->temp_paint;
267 |
268 | if (!paint) {
269 | return;
270 | }
271 |
272 | switch (paint->type) {
273 | case SWAPPY_PAINT_MODE_TEXT:
274 | if (g_utf8_strlen(paint->content.text.text, -1) == 0) {
275 | paint->can_draw = false;
276 | }
277 | paint->content.text.mode = SWAPPY_TEXT_MODE_DONE;
278 | break;
279 | default:
280 | break;
281 | }
282 |
283 | if (!paint->can_draw) {
284 | paint_free(paint);
285 | } else {
286 | paint->is_committed = true;
287 | state->paints = g_list_prepend(state->paints, paint);
288 | }
289 |
290 | // Set the temporary paint to NULL but keep the content in memory
291 | // because it's now part of the GList.
292 | state->temp_paint = NULL;
293 | }
294 |
--------------------------------------------------------------------------------
/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 | gboolean has_alpha = gdk_pixbuf_get_has_alpha(image);
97 | cairo_format_t format = has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24;
98 | gint image_width = gdk_pixbuf_get_width(image);
99 | gint image_height = gdk_pixbuf_get_height(image);
100 |
101 | cairo_surface_t *original_image_surface =
102 | cairo_image_surface_create(format, image_width, image_height);
103 |
104 | if (!original_image_surface) {
105 | g_error("unable to create cairo original surface from pixbuf");
106 | goto finish;
107 | } else {
108 | cairo_t *cr;
109 | cr = cairo_create(original_image_surface);
110 | gdk_cairo_set_source_pixbuf(cr, image, 0, 0);
111 | cairo_paint(cr);
112 | cairo_destroy(cr);
113 | }
114 |
115 | cairo_surface_t *rendering_surface =
116 | cairo_image_surface_create(format, image_width, image_height);
117 |
118 | if (!rendering_surface) {
119 | g_error("unable to create rendering surface");
120 | goto finish;
121 | }
122 |
123 | g_info("size of area to render: %ux%u", alloc->width, alloc->height);
124 |
125 | finish:
126 | if (state->original_image_surface) {
127 | cairo_surface_destroy(state->original_image_surface);
128 | state->original_image_surface = NULL;
129 | }
130 | state->original_image_surface = original_image_surface;
131 |
132 | if (state->rendering_surface) {
133 | cairo_surface_destroy(state->rendering_surface);
134 | state->rendering_surface = NULL;
135 | }
136 | state->rendering_surface = rendering_surface;
137 |
138 | g_free(alloc);
139 | }
140 |
141 | void pixbuf_free(struct swappy_state *state) {
142 | if (G_IS_OBJECT(state->original_image)) {
143 | g_object_unref(state->original_image);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/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: 2022-11-18 16:07-0500\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:456
21 | msgid "Line Width"
22 | msgstr "Linienstärke"
23 |
24 | #: res/swappy.glade:526
25 | msgid "Text Size"
26 | msgstr "Textgröße"
27 |
28 | #: res/swappy.glade:592
29 | msgid "Fill shape"
30 | msgstr ""
31 |
32 | #: res/swappy.glade:597
33 | msgid "Toggle shape filling"
34 | msgstr ""
35 |
36 | #: res/swappy.glade:671
37 | msgid "Toggle Paint Panel"
38 | msgstr "Farbtafel umschalten"
39 |
40 | #: res/swappy.glade:697
41 | msgid "Undo Last Paint"
42 | msgstr "Letzte Bemalung rückgängig machen"
43 |
44 | #: res/swappy.glade:716
45 | msgid "Redo Previous Paint"
46 | msgstr "Vorherige Bemalung wiederherstellen"
47 |
48 | #: res/swappy.glade:735
49 | msgid "Clear Paints"
50 | msgstr "Bemalung löschen"
51 |
52 | #: res/swappy.glade:763
53 | msgid "Copy Surface"
54 | msgstr "Fläche kopieren"
55 |
56 | #: res/swappy.glade:779
57 | msgid "Save Surface"
58 | msgstr "Fläche speichern"
59 |
--------------------------------------------------------------------------------
/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: 2022-11-18 16:07-0500\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:456
21 | msgid "Line Width"
22 | msgstr "Line Width"
23 |
24 | #: res/swappy.glade:526
25 | msgid "Text Size"
26 | msgstr "Text Size"
27 |
28 | #: res/swappy.glade:592
29 | msgid "Fill shape"
30 | msgstr "Fill shape"
31 |
32 | #: res/swappy.glade:597
33 | msgid "Toggle shape filling"
34 | msgstr "Toggle shape filling"
35 |
36 | #: res/swappy.glade:671
37 | msgid "Toggle Paint Panel"
38 | msgstr "Toggle Paint Panel"
39 |
40 | #: res/swappy.glade:697
41 | msgid "Undo Last Paint"
42 | msgstr "Undo Last Paint"
43 |
44 | #: res/swappy.glade:716
45 | msgid "Redo Previous Paint"
46 | msgstr "Redo Previous Paint"
47 |
48 | #: res/swappy.glade:735
49 | msgid "Clear Paints"
50 | msgstr "Clear Paints"
51 |
52 | #: res/swappy.glade:763
53 | msgid "Copy Surface"
54 | msgstr "Copy Surface"
55 |
56 | #: res/swappy.glade:779
57 | msgid "Save Surface"
58 | msgstr "Save Surface"
59 |
--------------------------------------------------------------------------------
/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: 2022-11-18 16:07-0500\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:456
21 | msgid "Line Width"
22 | msgstr "Epaisseur de ligne"
23 |
24 | #: res/swappy.glade:526
25 | msgid "Text Size"
26 | msgstr "Taille du texte"
27 |
28 | #: res/swappy.glade:592
29 | msgid "Fill shape"
30 | msgstr "Remplir la forme"
31 |
32 | #: res/swappy.glade:597
33 | msgid "Toggle shape filling"
34 | msgstr "Activer/Désactiver le remplissage de forme"
35 |
36 | #: res/swappy.glade:671
37 | msgid "Toggle Paint Panel"
38 | msgstr "Afficher/Cacher le panneau de peinture"
39 |
40 | #: res/swappy.glade:697
41 | msgid "Undo Last Paint"
42 | msgstr "Annuler la dernière peinture"
43 |
44 | #: res/swappy.glade:716
45 | msgid "Redo Previous Paint"
46 | msgstr "Rétablir la dernière peinture"
47 |
48 | #: res/swappy.glade:735
49 | msgid "Clear Paints"
50 | msgstr "Supprimer les peintures"
51 |
52 | #: res/swappy.glade:763
53 | msgid "Copy Surface"
54 | msgstr "Copier la surface"
55 |
56 | #: res/swappy.glade:779
57 | msgid "Save Surface"
58 | msgstr "Sauvegarder la surface"
59 |
--------------------------------------------------------------------------------
/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: 2022-11-18 16:07-0500\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:456
22 | msgid "Line Width"
23 | msgstr "Espessura da linha"
24 |
25 | #: res/swappy.glade:526
26 | msgid "Text Size"
27 | msgstr "Tamanho do texto"
28 |
29 | #: res/swappy.glade:592
30 | msgid "Fill shape"
31 | msgstr ""
32 |
33 | #: res/swappy.glade:597
34 | msgid "Toggle shape filling"
35 | msgstr ""
36 |
37 | #: res/swappy.glade:671
38 | msgid "Toggle Paint Panel"
39 | msgstr "Alternar painel de pintura"
40 |
41 | #: res/swappy.glade:697
42 | msgid "Undo Last Paint"
43 | msgstr "Desfazer última pintura"
44 |
45 | #: res/swappy.glade:716
46 | msgid "Redo Previous Paint"
47 | msgstr "Refazer pintura anterior"
48 |
49 | #: res/swappy.glade:735
50 | msgid "Clear Paints"
51 | msgstr "Limpar pinturas"
52 |
53 | #: res/swappy.glade:763
54 | msgid "Copy Surface"
55 | msgstr "Copiar superfície"
56 |
57 | #: res/swappy.glade:779
58 | msgid "Save Surface"
59 | msgstr "Salvar superfície"
60 |
--------------------------------------------------------------------------------
/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: 2022-11-18 16:07-0500\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:456
21 | msgid "Line Width"
22 | msgstr ""
23 |
24 | #: res/swappy.glade:526
25 | msgid "Text Size"
26 | msgstr ""
27 |
28 | #: res/swappy.glade:592
29 | msgid "Fill shape"
30 | msgstr ""
31 |
32 | #: res/swappy.glade:597
33 | msgid "Toggle shape filling"
34 | msgstr ""
35 |
36 | #: res/swappy.glade:671
37 | msgid "Toggle Paint Panel"
38 | msgstr ""
39 |
40 | #: res/swappy.glade:697
41 | msgid "Undo Last Paint"
42 | msgstr ""
43 |
44 | #: res/swappy.glade:716
45 | msgid "Redo Previous Paint"
46 | msgstr ""
47 |
48 | #: res/swappy.glade:735
49 | msgid "Clear Paints"
50 | msgstr ""
51 |
52 | #: res/swappy.glade:763
53 | msgid "Copy Surface"
54 | msgstr ""
55 |
56 | #: res/swappy.glade:779
57 | msgid "Save Surface"
58 | msgstr ""
59 |
--------------------------------------------------------------------------------
/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: 2022-11-18 16:07-0500\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:456
21 | msgid "Line Width"
22 | msgstr "Çizgi Genişliği"
23 |
24 | #: res/swappy.glade:526
25 | msgid "Text Size"
26 | msgstr "Metin Boyutu"
27 |
28 | #: res/swappy.glade:592
29 | msgid "Fill shape"
30 | msgstr "Şekli Doldur"
31 |
32 | #: res/swappy.glade:597
33 | msgid "Toggle shape filling"
34 | msgstr "Şekil Doldurmayı Aç/Kapat"
35 |
36 | #: res/swappy.glade:671
37 | msgid "Toggle Paint Panel"
38 | msgstr "Boyama Panelini Aç/Kapat"
39 |
40 | #: res/swappy.glade:697
41 | msgid "Undo Last Paint"
42 | msgstr "Son Boyamayı Geri Al"
43 |
44 | #: res/swappy.glade:716
45 | msgid "Redo Previous Paint"
46 | msgstr "Önceki Boyamayı Tekrarla"
47 |
48 | #: res/swappy.glade:735
49 | msgid "Clear Paints"
50 | msgstr "Boyamaları Temizle"
51 |
52 | #: res/swappy.glade:763
53 | msgid "Copy Surface"
54 | msgstr "Yüzeyi Kopyala"
55 |
56 | #: res/swappy.glade:779
57 | msgid "Save Surface"
58 | msgstr "Yüzeyi Kaydet"
59 |
--------------------------------------------------------------------------------
/src/po/zh_CN.po:
--------------------------------------------------------------------------------
1 |
2 | # English translations for swappy package.
3 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER
4 | # This file is distributed under the same license as the swappy package.
5 | # Automatically generated, 2020.
6 | #
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: swappy\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2022-11-18 16:07-0500\n"
12 | "PO-Revision-Date: 2020-06-21 21:57-0400\n"
13 | "Last-Translator: Automatically generated\n"
14 | "Language-Team: none\n"
15 | "Language: zh_CN\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=1; plural=0;\n"
20 |
21 | #: res/swappy.glade:456
22 | msgid "Line Width"
23 | msgstr "行宽"
24 |
25 | #: res/swappy.glade:526
26 | msgid "Text Size"
27 | msgstr "文本大小"
28 |
29 | #: res/swappy.glade:592
30 | msgid "Fill shape"
31 | msgstr "填充"
32 |
33 | #: res/swappy.glade:597
34 | msgid "Toggle shape filling"
35 | msgstr "切换填充状态"
36 |
37 | #: res/swappy.glade:671
38 | msgid "Toggle Paint Panel"
39 | msgstr "切换绘图板状态"
40 |
41 | #: res/swappy.glade:697
42 | msgid "Undo Last Paint"
43 | msgstr "撤销"
44 |
45 | #: res/swappy.glade:716
46 | msgid "Redo Previous Paint"
47 | msgstr "恢复"
48 |
49 | #: res/swappy.glade:735
50 | msgid "Clear Paints"
51 | msgstr "清除绘图"
52 |
53 | #: res/swappy.glade:763
54 | msgid "Copy Surface"
55 | msgstr "复制"
56 |
57 | #: res/swappy.glade:779
58 | msgid "Save Surface"
59 | msgstr "保存"
60 |
--------------------------------------------------------------------------------
/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 | char pango_font[255];
184 | double x = fmin(text.from.x, text.to.x);
185 | double y = fmin(text.from.y, text.to.y);
186 | double w = fabs(text.from.x - text.to.x);
187 | double h = fabs(text.from.y - text.to.y);
188 |
189 | cairo_surface_t *surface =
190 | cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
191 | cairo_t *crt = cairo_create(surface);
192 |
193 | pango_layout_t *layout = pango_cairo_create_layout(crt);
194 | pango_layout_set_text(layout, text.text, -1);
195 | g_snprintf(pango_font, 255, "%s %d", text.font, (int)text.s);
196 | pango_font_description_t *desc =
197 | pango_font_description_from_string(pango_font);
198 | pango_layout_set_width(layout, pango_units_from_double(w));
199 | pango_layout_set_font_description(layout, desc);
200 | pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
201 | pango_font_description_free(desc);
202 |
203 | if (text.mode == SWAPPY_TEXT_MODE_EDIT) {
204 | pango_rectangle_t strong_pos;
205 | struct swappy_box cursor_box;
206 | cairo_set_source_rgba(cr, 0.5, 0.5, 0.5, 0.3);
207 | cairo_set_line_width(cr, 5);
208 | cairo_rectangle(cr, x, y, w, h);
209 | cairo_stroke(cr);
210 | glong bytes_til_cursor = string_get_nb_bytes_until(text.text, text.cursor);
211 | pango_layout_get_cursor_pos(layout, bytes_til_cursor, &strong_pos, NULL);
212 | convert_pango_rectangle_to_swappy_box(strong_pos, &cursor_box);
213 | cairo_move_to(crt, cursor_box.x, cursor_box.y);
214 | cairo_set_source_rgba(crt, 0.3, 0.3, 0.3, 1);
215 | cairo_line_to(crt, cursor_box.x, cursor_box.y + cursor_box.height);
216 | cairo_stroke(crt);
217 | }
218 |
219 | cairo_rectangle(crt, 0, 0, w, h);
220 | cairo_set_source_rgba(crt, text.r, text.g, text.b, text.a);
221 | cairo_move_to(crt, 0, 0);
222 | pango_cairo_show_layout(crt, layout);
223 |
224 | cairo_set_source_surface(cr, surface, x, y);
225 | cairo_paint(cr);
226 |
227 | cairo_destroy(crt);
228 | cairo_surface_destroy(surface);
229 | g_object_unref(layout);
230 | }
231 |
232 | static void render_shape_arrow(cairo_t *cr, struct swappy_paint_shape shape) {
233 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a);
234 | cairo_set_line_width(cr, shape.w);
235 |
236 | double ftx = shape.to.x - shape.from.x;
237 | double fty = shape.to.y - shape.from.y;
238 | double ftn = sqrt(ftx * ftx + fty * fty);
239 |
240 | double r = 20;
241 | double scaling_factor = shape.w / 4;
242 |
243 | double alpha = G_PI / 6;
244 | double ta = 5 * alpha;
245 | double tb = 7 * alpha;
246 | double xa = r * cos(ta);
247 | double ya = r * sin(ta);
248 | double xb = r * cos(tb);
249 | double yb = r * sin(tb);
250 | double xc = ftn - fabs(xa) * scaling_factor;
251 |
252 | if (xc < DBL_EPSILON) {
253 | xc = 0;
254 | }
255 |
256 | if (ftn < DBL_EPSILON) {
257 | return;
258 | }
259 |
260 | double theta = copysign(1.0, fty) * acos(ftx / ftn);
261 |
262 | // Draw line
263 | cairo_save(cr);
264 | cairo_translate(cr, shape.from.x, shape.from.y);
265 | cairo_rotate(cr, theta);
266 | cairo_move_to(cr, 0, 0);
267 | cairo_line_to(cr, xc, 0);
268 | cairo_stroke(cr);
269 | cairo_restore(cr);
270 |
271 | // Draw arrow
272 | cairo_save(cr);
273 | cairo_translate(cr, shape.to.x, shape.to.y);
274 | cairo_rotate(cr, theta);
275 | cairo_scale(cr, scaling_factor, scaling_factor);
276 | cairo_move_to(cr, 0, 0);
277 | cairo_line_to(cr, xa, ya);
278 | cairo_line_to(cr, xb, yb);
279 | cairo_line_to(cr, 0, 0);
280 | cairo_fill(cr);
281 | cairo_restore(cr);
282 | }
283 |
284 | static void render_shape_ellipse(cairo_t *cr, struct swappy_paint_shape shape) {
285 | double x = fabs(shape.from.x - shape.to.x);
286 | double y = fabs(shape.from.y - shape.to.y);
287 |
288 | double n = sqrt(x * x + y * y);
289 |
290 | double xc, yc, r;
291 |
292 | if (shape.should_center_at_from) {
293 | xc = shape.from.x;
294 | yc = shape.from.y;
295 |
296 | r = n;
297 | } else {
298 | xc = shape.from.x + ((shape.to.x - shape.from.x) / 2);
299 | yc = shape.from.y + ((shape.to.y - shape.from.y) / 2);
300 |
301 | r = n / 2;
302 | }
303 |
304 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a);
305 | cairo_set_line_width(cr, shape.w);
306 |
307 | cairo_matrix_t save_matrix;
308 | cairo_get_matrix(cr, &save_matrix);
309 | cairo_translate(cr, xc, yc);
310 | cairo_scale(cr, x / n, y / n);
311 | cairo_arc(cr, 0, 0, r, 0, 2 * G_PI);
312 | cairo_set_matrix(cr, &save_matrix);
313 |
314 | switch (shape.operation) {
315 | case SWAPPY_PAINT_SHAPE_OPERATION_STROKE:
316 | cairo_stroke(cr);
317 | break;
318 | case SWAPPY_PAINT_SHAPE_OPERATION_FILL:
319 | cairo_fill(cr);
320 | break;
321 | default:
322 | cairo_stroke(cr);
323 | break;
324 | }
325 |
326 | cairo_close_path(cr);
327 | }
328 |
329 | static void render_shape_rectangle(cairo_t *cr,
330 | struct swappy_paint_shape shape) {
331 | double x, y, w, h;
332 |
333 | if (shape.should_center_at_from) {
334 | x = shape.from.x - fabs(shape.from.x - shape.to.x);
335 | y = shape.from.y - fabs(shape.from.y - shape.to.y);
336 | w = fabs(shape.from.x - shape.to.x) * 2;
337 | h = fabs(shape.from.y - shape.to.y) * 2;
338 | } else {
339 | x = fmin(shape.from.x, shape.to.x);
340 | y = fmin(shape.from.y, shape.to.y);
341 | w = fabs(shape.from.x - shape.to.x);
342 | h = fabs(shape.from.y - shape.to.y);
343 | }
344 |
345 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a);
346 | cairo_set_line_width(cr, shape.w);
347 |
348 | cairo_rectangle(cr, x, y, w, h);
349 | cairo_close_path(cr);
350 |
351 | switch (shape.operation) {
352 | case SWAPPY_PAINT_SHAPE_OPERATION_STROKE:
353 | cairo_stroke(cr);
354 | break;
355 | case SWAPPY_PAINT_SHAPE_OPERATION_FILL:
356 | cairo_fill(cr);
357 | break;
358 | default:
359 | cairo_stroke(cr);
360 | break;
361 | }
362 | }
363 |
364 | static void render_shape(cairo_t *cr, struct swappy_paint_shape shape) {
365 | cairo_save(cr);
366 | switch (shape.type) {
367 | case SWAPPY_PAINT_MODE_RECTANGLE:
368 | render_shape_rectangle(cr, shape);
369 | break;
370 | case SWAPPY_PAINT_MODE_ELLIPSE:
371 | render_shape_ellipse(cr, shape);
372 | break;
373 | case SWAPPY_PAINT_MODE_ARROW:
374 | render_shape_arrow(cr, shape);
375 | break;
376 | default:
377 | break;
378 | }
379 | cairo_restore(cr);
380 | }
381 |
382 | static void render_background(cairo_t *cr, struct swappy_state *state) {
383 | cairo_set_source_rgb(cr, 0, 0, 0);
384 | cairo_paint(cr);
385 | }
386 |
387 | static void render_blur(cairo_t *cr, struct swappy_paint *paint) {
388 | struct swappy_paint_blur blur = paint->content.blur;
389 |
390 | cairo_surface_t *target = cairo_get_target(cr);
391 |
392 | double x = MIN(blur.from.x, blur.to.x);
393 | double y = MIN(blur.from.y, blur.to.y);
394 | double w = ABS(blur.from.x - blur.to.x);
395 | double h = ABS(blur.from.y - blur.to.y);
396 |
397 | cairo_save(cr);
398 |
399 | if (paint->is_committed) {
400 | // Surface has already been blurred, reuse it in future passes
401 | if (blur.surface) {
402 | cairo_surface_t *surface = blur.surface;
403 | if (surface && cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) {
404 | cairo_set_source_surface(cr, surface, x, y);
405 | cairo_paint(cr);
406 | }
407 | } else {
408 | // Blur surface and reuse it in future passes
409 | g_info(
410 | "blurring surface on following image coordinates: %.2lf,%.2lf size: "
411 | "%.2lfx%.2lf",
412 | x, y, w, h);
413 | cairo_surface_t *blurred = blur_surface(target, x, y, w, h);
414 |
415 | if (blurred && cairo_surface_status(blurred) == CAIRO_STATUS_SUCCESS) {
416 | cairo_set_source_surface(cr, blurred, x, y);
417 | cairo_paint(cr);
418 | paint->content.blur.surface = blurred;
419 | }
420 | }
421 | } else {
422 | // Blur not committed yet, draw bounding rectangle
423 | struct swappy_paint_shape rect = {
424 | .r = 0,
425 | .g = 0.5,
426 | .b = 1,
427 | .a = 0.5,
428 | .w = 5,
429 | .from = blur.from,
430 | .to = blur.to,
431 | .type = SWAPPY_PAINT_MODE_RECTANGLE,
432 | .operation = SWAPPY_PAINT_SHAPE_OPERATION_FILL,
433 | };
434 | render_shape_rectangle(cr, rect);
435 | }
436 |
437 | cairo_restore(cr);
438 | }
439 |
440 | static void render_brush(cairo_t *cr, struct swappy_paint_brush brush) {
441 | cairo_set_source_rgba(cr, brush.r, brush.g, brush.b, brush.a);
442 | cairo_set_line_width(cr, brush.w);
443 | cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL);
444 |
445 | guint l = g_list_length(brush.points);
446 |
447 | if (l == 1) {
448 | struct swappy_point *point = g_list_nth_data(brush.points, 0);
449 | cairo_rectangle(cr, point->x, point->y, brush.w, brush.w);
450 | cairo_fill(cr);
451 | } else {
452 | for (GList *elem = brush.points; elem; elem = elem->next) {
453 | struct swappy_point *point = elem->data;
454 | cairo_line_to(cr, point->x, point->y);
455 | }
456 | cairo_stroke(cr);
457 | }
458 | }
459 |
460 | static void render_image(cairo_t *cr, struct swappy_state *state) {
461 | cairo_surface_t *surface = state->original_image_surface;
462 |
463 | cairo_save(cr);
464 |
465 | if (surface && !cairo_surface_status(surface)) {
466 | cairo_set_source_surface(cr, surface, 0, 0);
467 | cairo_paint(cr);
468 | }
469 |
470 | cairo_restore(cr);
471 | }
472 |
473 | static void render_paint(cairo_t *cr, struct swappy_paint *paint) {
474 | if (!paint->can_draw) {
475 | return;
476 | }
477 | switch (paint->type) {
478 | case SWAPPY_PAINT_MODE_BLUR:
479 | render_blur(cr, paint);
480 | break;
481 | case SWAPPY_PAINT_MODE_BRUSH:
482 | render_brush(cr, paint->content.brush);
483 | break;
484 | case SWAPPY_PAINT_MODE_RECTANGLE:
485 | case SWAPPY_PAINT_MODE_ELLIPSE:
486 | case SWAPPY_PAINT_MODE_ARROW:
487 | render_shape(cr, paint->content.shape);
488 | break;
489 | case SWAPPY_PAINT_MODE_TEXT:
490 | render_text(cr, paint->content.text);
491 | break;
492 | default:
493 | g_info("unable to render paint with type: %d", paint->type);
494 | break;
495 | }
496 | }
497 |
498 | static void render_paints(cairo_t *cr, struct swappy_state *state) {
499 | for (GList *elem = g_list_last(state->paints); elem; elem = elem->prev) {
500 | struct swappy_paint *paint = elem->data;
501 | render_paint(cr, paint);
502 | }
503 |
504 | if (state->temp_paint) {
505 | render_paint(cr, state->temp_paint);
506 | }
507 | }
508 |
509 | void render_state(struct swappy_state *state) {
510 | cairo_surface_t *surface = state->rendering_surface;
511 | cairo_t *cr = cairo_create(surface);
512 |
513 | render_background(cr, state);
514 | render_image(cr, state);
515 | render_paints(cr, state);
516 |
517 | cairo_destroy(cr);
518 |
519 | // Drawing is finished, notify the GtkDrawingArea it needs to be redrawn.
520 | gtk_widget_queue_draw(state->ui->area);
521 | }
522 |
--------------------------------------------------------------------------------
/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 | gchar *new_str = g_new0(gchar, MAX(str_len, 1));
9 | gchar *buffer_source = str;
10 | gchar *buffer_copy = new_str;
11 | glong i = 0;
12 | gint bytes;
13 | gunichar c;
14 |
15 | if (pos <= str_len && g_utf8_validate(str, -1, NULL)) {
16 | while (*buffer_source != '\0') {
17 | c = g_utf8_get_char(buffer_source);
18 | buffer_source = g_utf8_next_char(buffer_source);
19 | if (i != pos) {
20 | bytes = g_unichar_to_utf8(c, buffer_copy);
21 | buffer_copy += bytes;
22 | }
23 | i++;
24 | }
25 | }
26 |
27 | return new_str;
28 | }
29 |
30 | gchar *string_insert_chars_at(gchar *str, gchar *chars, glong pos) {
31 | gchar *new_str = NULL;
32 |
33 | if (g_utf8_validate(str, -1, NULL) && g_utf8_validate(chars, -1, NULL) &&
34 | pos >= 0 && pos <= g_utf8_strlen(str, -1)) {
35 | gchar *from = g_utf8_substring(str, 0, pos);
36 | gchar *end = g_utf8_offset_to_pointer(str, pos);
37 |
38 | new_str = g_strconcat(from, chars, end, NULL);
39 |
40 | g_free(from);
41 |
42 | } else {
43 | new_str = g_new0(gchar, 1);
44 | }
45 |
46 | return new_str;
47 | }
48 |
49 | glong string_get_nb_bytes_until(gchar *str, glong until) {
50 | glong ret = 0;
51 | if (str) {
52 | gchar *sub = g_utf8_substring(str, 0, until);
53 | ret = strlen(sub);
54 | g_free(sub);
55 | }
56 |
57 | return ret;
58 | }
59 |
60 | void pixel_data_print(guint32 pixel) {
61 | const guint32 r = pixel >> 24 & 0xff;
62 | const guint32 g = pixel >> 16 & 0xff;
63 | const guint32 b = pixel >> 8 & 0xff;
64 | const guint32 a = pixel >> 0 & 0xff;
65 |
66 | g_debug("rgba(%u, %d, %u, %u)", r, g, b, a);
67 | }
68 |
--------------------------------------------------------------------------------
/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 | ```
72 |
73 | - *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
74 | - *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
75 | - *show_panel* is used to toggle the paint panel on or off upon startup
76 | - *line_size* is the default line size (must be between 1 and 50)
77 | - *text_size* is the default text size (must be between 10 and 50)
78 | - *text_font* is the font used to render text, its format is pango friendly
79 | - *paint_mode* is the mode activated at application start (must be one of: brush|text|rectangle|ellipse|arrow|blur, matching is case-insensitive)
80 | - *early_exit* is used to make the application exit after saving the picture or copying it to the clipboard
81 | - *fill_shape* is used to toggle shape filling (for the rectangle and ellipsis tools) on or off upon startup
82 | - *auto_save* is used to toggle auto saving of final buffer to *save_dir* upon exit
83 | - *custom_color* is used to set a default value for the custom color. Accepted
84 | 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)
85 |
86 |
87 | # KEY BINDINGS
88 |
89 | ## LAYOUT
90 |
91 | - *Ctrl+b*: Toggle Paint Panel
92 |
93 | ## PAINT MODE
94 |
95 | - *b*: Switch to Brush
96 | - *t*: Switch to Text
97 | - *r*: Switch to Rectangle
98 | - *o*: Switch to Ellipse
99 | - *a*: Switch to Arrow
100 | - *d*: Switch to Blur (d stands for droplet)
101 |
102 | - *R*: Use Red Color
103 | - *G*: Use Green Color
104 | - *B*: Use Blue Color
105 | - *C*: Use Custom Color
106 | - *Minus*: Reduce Stroke Size
107 | - *Plus*: Increase Stroke Size
108 | - *Equal*: Reset Stroke Size
109 | - *f*: Toggle Shape Filling
110 | - *k*: Clear Paints (cannot be undone)
111 |
112 | ## MODIFIERS
113 |
114 | - *Ctrl*: Center Shape (Rectangle & Ellipse) based on draw start
115 |
116 | ## HEADER BAR
117 |
118 | - *Ctrl+z*: Undo
119 | - *Ctrl+Shift+z* or *Ctrl+y*: Redo
120 | - *Ctrl+s*: Save to file (see man page)
121 | - *Ctrl+c*: Copy to clipboard
122 | - *Escape* or *q* or *Ctrl+w*: Quit swappy
123 |
124 | # AUTHORS
125 |
126 | Written and maintained by jtheoof . See
127 | https://github.com/jtheoof/swappy.
128 |
--------------------------------------------------------------------------------
/test/images/large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/test/images/large.png
--------------------------------------------------------------------------------
/test/images/passwords.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/test/images/passwords.png
--------------------------------------------------------------------------------
/test/images/small-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/test/images/small-blue.png
--------------------------------------------------------------------------------