├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── COPYING
├── README.md
├── build-aux
└── meson
│ └── postinstall.py
├── com.github.hezral.quickword.yml
├── data
├── application.css
├── com.github.hezral.quickword.appdata.xml.in
├── com.github.hezral.quickword.desktop.in
├── com.github.hezral.quickword.gschema.xml
├── demo.gif
├── icons
│ ├── 128.svg
│ ├── 16-symbolic.svg
│ ├── 16.svg
│ ├── 24.svg
│ ├── 32.svg
│ ├── 48.svg
│ ├── 64.svg
│ ├── 96.svg
│ ├── com.github.hezral.quickword-coffee.svg
│ ├── com.github.hezral.quickword-left.svg
│ ├── com.github.hezral.quickword-right.svg
│ ├── com.github.hezral.quickword-symbolic.svg
│ ├── com.github.hezral.quickword.svg
│ └── prep_icons.py
├── logo_type.png
├── meson.build
├── screenshot-01.png
├── screenshot-02.png
└── screenshot-03.png
├── debian
├── changelog
├── compat
├── control
├── copyright
├── rules
└── source
│ └── format
├── meson.build
├── po
├── LINGUAS
├── POTFILES
└── meson.build
└── src
├── __init__.py
├── active_window_manager.py
├── clipboard_manager.py
├── data_manager.py
├── main.py
├── main_window.py
├── meson.build
├── noword_view.py
├── quickword.in
├── settings_view.py
├── updater_view.py
├── utils.py
├── word_lookup.py
└── word_view.py
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | # This workflow will run for any pull request or pushed commit
4 | on: [push, pull_request]
5 |
6 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
7 | jobs:
8 | # This workflow contains a single job called "flatpak"
9 | flatpak:
10 | # The type of runner that the job will run on
11 | runs-on: ubuntu-latest
12 |
13 | # This job runs in a special container designed for building Flatpaks for AppCenter
14 | container:
15 | image: ghcr.io/elementary/flatpak-platform/runtime:6
16 | options: --privileged
17 |
18 | # Steps represent a sequence of tasks that will be executed as part of the job
19 | steps:
20 | # Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it
21 | - uses: actions/checkout@v2
22 |
23 | # Builds your flatpak manifest using the Flatpak Builder action
24 | - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4
25 | with:
26 | # This is the name of the Bundle file we're building and can be anything you like
27 | bundle: quickword.flatpak
28 | # This uses your app's RDNN ID
29 | manifest-path: com.github.hezral.quickword.yml
30 |
31 | # You can automatically run any of the tests you've created as part of this workflow
32 | run-tests: false
33 |
34 | # These lines specify the location of the elementary Runtime and Sdk
35 | repository-name: appcenter
36 | repository-url: https://flatpak.elementary.io/repo.flatpakrepo
37 | cache-key: "flatpak-builder-${{ github.sha }}"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # Distribution / packaging
7 | .Python
8 | build/
9 | develop-eggs/
10 | dist/
11 | downloads/
12 | eggs/
13 | .eggs/
14 | lib/
15 | lib64/
16 | parts/
17 | sdist/
18 | var/
19 | wheels/
20 | *.egg-info/
21 | .installed.cfg
22 | *.egg
23 | MANIFEST
24 |
25 | # Environments
26 | .env
27 | .venv
28 | env/
29 | venv/
30 | ENV/
31 | env.bak/
32 | venv.bak/
33 |
34 | # Flatpak
35 | .flatpak/
36 | .flatpak-builder/
37 | build-dir/
38 |
39 | # VSCode
40 | .vscode/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | # 
6 |
7 |
8 | If you like what i make, it would really be nice to have someone buy me a coffee
9 |
10 |
11 |
12 | ### Quickly lookup words on the fly and offline
13 | * On the fly word lookup
14 | * Works offline (first run will require internet to download dictionary data)
15 | * Copy definitions and examples to clipboard with just a click
16 | * Word definitions
17 | * Examples of word sentences
18 | * Synonyms
19 | * Explore synonyms from each word
20 |
21 | |  |  |  |
22 | |------------------------------------------|-----------------------------------------|-----------------------------------------|
23 |
24 | ## Demo and How to use
25 | QuickWord can be used by manual lookup and via a keyboard shortcut to open it on the fly.
26 |
27 | Select a word and hit the a keyboard shortcut to get the lookup. Recommended shortcut: ⌘ + D
28 | * Manual entry of words
29 | * Selecting a text
30 |
31 | 
32 |
33 | # Installation
34 | QuickWord is availble for installation in the following Linux Distributions
35 |
36 |
37 |
38 |
39 |
40 | ## Build using flatpak
41 | * requires that you have flatpak-builder installed
42 | * flatpak enabled
43 | * flathub remote enabled
44 |
45 | ```
46 | flatpak-builder --user --force-clean --install build-dir com.github.hezral.quickword.yml
47 | ```
48 |
49 | ### Build using meson
50 | Ensure you have these dependencies installed
51 |
52 | * python3
53 | * python3-gi
54 | * libgranite-dev
55 | * python-xlib
56 | * xclip
57 | * pynput
58 |
59 | Download the updated source [here](https://github.com/hezral/quickword/archive/master.zip), or use git:
60 | ```bash
61 | git clone https://github.com/hezral/quickword.git
62 | cd clips
63 | meson build --prefix=/usr
64 | cd build
65 | ninja build
66 | sudo ninja install
67 | ```
68 | The desktop launcher should show up on the application launcher for your desktop environment
69 | if it doesn't, try running
70 | ```
71 | com.github.hezral.quickword
72 | ```
73 |
74 | ## Thanks/Credits
75 | - [Wordnet](http://wordnetweb.princeton.edu/perl/webwn) Definitions are based on Wordnet.
76 | - [Ideogram](https://appcenter.elementary.io/com.github.cassidyjames.ideogram/) Inspired by it. Also borrowed/forked some code.
77 | - [ElementaryPython](https://github.com/mirkobrombin/ElementaryPython) This started me off on coding with Python and GTK.
78 |
--------------------------------------------------------------------------------
/build-aux/meson/postinstall.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from os import environ, path
4 | from subprocess import call
5 |
6 | prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
7 | datadir = path.join(prefix, 'share')
8 | destdir = environ.get('DESTDIR', '')
9 |
10 | # Package managers set this so we don't need to run
11 | if not destdir:
12 | print('Updating icon cache...')
13 | call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
14 |
15 | print('Updating desktop database...')
16 | call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
17 |
18 | print('Compiling GSettings schemas...')
19 | call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])
20 |
21 |
22 |
--------------------------------------------------------------------------------
/com.github.hezral.quickword.yml:
--------------------------------------------------------------------------------
1 | app-id: com.github.hezral.quickword
2 | runtime: io.elementary.Platform
3 | runtime-version: '6'
4 | sdk: io.elementary.Sdk
5 | command: com.github.hezral.quickword
6 | finish-args:
7 | # for espeak to work
8 | - --socket=pulseaudio
9 | - --share=ipc
10 | - --socket=wayland
11 | - --socket=fallback-x11
12 | - --share=network
13 |
14 | cleanup:
15 | - '/include'
16 | - '/lib/pkgconfig'
17 | - '/lib/cmake'
18 | - '/lib/girepository-1.0'
19 | - '/share/gir-1.0'
20 | - '/share/vala'
21 | - '*.a'
22 | - '*.la'
23 | modules:
24 | - name: python-xlib
25 | buildsystem: simple
26 | build-options:
27 | build-args:
28 | - --share=network
29 | build-commands:
30 | - "pip3 install --prefix=${FLATPAK_DEST} python-xlib"
31 |
32 | - name: ntlk
33 | buildsystem: simple
34 | build-options:
35 | build-args:
36 | - --share=network
37 | build-commands:
38 | - "pip3 install --prefix=${FLATPAK_DEST} nltk"
39 |
40 | - name: pcaudiolib
41 | buildsystem: autotools
42 | sources:
43 | - type: git
44 | url: https://github.com/espeak-ng/pcaudiolib.git
45 |
46 | # this is the version the builds with flatpak
47 | - name: espeak-ng
48 | buildsystem: simple
49 | build-commands:
50 | - "./autogen.sh"
51 | - "CC=clang CFLAGS=-Wextra ./configure --prefix=${FLATPAK_DEST}"
52 | - "make"
53 | - "make install"
54 | sources:
55 | - type: git
56 | url: https://github.com/espeak-ng/espeak-ng.git
57 | tag: "1.50"
58 | commit: "b702b03996de94035fadae0eb5ad9506c5a09f35"
59 |
60 | - name: quickword
61 | buildsystem: meson
62 | sources:
63 | - type: dir
64 | path: .
--------------------------------------------------------------------------------
/data/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | # SPDX-License-Identifier: GPL-3.0-or-later
3 | # SPDX-FileCopyrightText: 2021 Adi Hezral
4 | */
5 |
6 | /* -- colors -- */
7 | /* ------------------------------ */
8 | @define-color shaded_dark shade(@theme_base_color, 0.95);
9 | @define-color shaded_darker shade(@theme_base_color, 0.85);
10 | @define-color shaded_base shade(@theme_base_color, 0.98);
11 | @define-color popup shade(@theme_base_color, 0.98);
12 |
13 | /* ------------------------------ */
14 |
15 |
16 | /* -- headerbar -- */
17 | /* ------------------------------ */
18 | .lookup-word-header {
19 | font-size: larger;
20 | font-weight: bold;
21 | }
22 |
23 | /* -- stack -- */
24 | /* ------------------------------ */
25 | stack {
26 | border-bottom-right-radius: 4px;
27 | border-bottom-left-radius: 4px;
28 | }
29 |
30 |
31 | /* -- settings-view -- */
32 | /* ------------------------------ */
33 | label#settings-group-label {
34 | font-weight: bold;
35 | opacity: 0.75;
36 | }
37 |
38 | frame#settings-group-frame {
39 | border-radius: 4px;
40 | border-color: rgba(0, 0, 0, 0.3);
41 | background-color: @shaded_dark;
42 | }
43 |
44 | .settings-sub-label {
45 | font-size: 0.9em;
46 | color: gray;
47 | }
48 |
49 | /* -- quickword icon animation -- */
50 | /* ------------------------------ */
51 | .quickword-icon-left {
52 | animation: floating 1.5s ease-in-out infinite;
53 | }
54 |
55 | @keyframes float-down {
56 | 25% {-gtk-icon-transform: translateY(0px);}
57 | 75% {-gtk-icon-transform: translateY(-5px);}
58 | 100% {-gtk-icon-transform: translateX(0px);}
59 | }
60 |
61 | .quickword-icon-right {
62 | animation: floating 1.25s ease-in-out infinite;
63 | }
64 |
65 | @keyframes floating {
66 | 25% {-gtk-icon-transform: translateY(0px);}
67 | 75% {-gtk-icon-transform: translateY(3px);}
68 | 100% {-gtk-icon-transform: translateY(0px);}
69 | }
70 |
71 | @keyframes floating-two {
72 | from {
73 | -gtk-icon-transform: translateY(60px);
74 | }
75 | 25% {-gtk-icon-transform: translateY(60px);}
76 | 75% {-gtk-icon-transform: translateY(65px);}
77 | 100% {-gtk-icon-transform: translateY(60px);}
78 | }
79 |
80 | .download-icon-start {
81 | -gtk-icon-transform: translateY(60px);
82 | transition: -gtk-icon-transform 1.25s;
83 | animation: floating-two 1.25s ease-in-out infinite;
84 |
85 | }
86 |
87 | /* -- word-view -- */
88 | /* ------------------------------ */
89 | .word-types-left {
90 | border-top-left-radius: 0px;
91 | border-bottom-left-radius: 6px;
92 | border-right-color: rgba(0,0,0,0.05);
93 | }
94 |
95 | .word-types-right {
96 | border-top-right-radius: 0px;
97 | border-bottom-right-radius: 6px;
98 | border-left-color: rgba(0,0,0,0.05);
99 | }
100 |
101 | .word-types {
102 | border-top-color: rgba(0,0,0,0.1);
103 | border-bottom-color: rgba(0,0,0,0);
104 | border-left-color: rgba(0,0,0,0);
105 | border-right-color: rgba(0,0,0,0);
106 | color: alpha(@theme_text_color, 0.6);
107 | }
108 |
109 | .word-types:checked {
110 | font-weight: bold;
111 | color: alpha(@theme_text_color, 1);
112 | }
113 |
114 | .copied-content, .copy-img {
115 | font-size: 0.9em;
116 | color: white;
117 | background-color: rgba(24, 24, 24, 0.75);
118 | border-style: solid;
119 | padding: 4px;
120 | border-radius: 4px;
121 | }
122 |
123 | .word-hover {
124 | color: @accent_color_500;
125 | }
126 |
127 | .word-examples {
128 | font-style: italic;
129 | color: gray;
130 | }
131 |
132 | .word-examples-hover {
133 | font-style: italic;
134 | color: alpha(@accent_color_500, 0.75);
135 | }
136 |
137 | .word-lemmas {
138 | opacity: 0.7;
139 | font-size: 0.9em;
140 | border-style: solid;
141 | border-width: 1px;
142 | padding-left: 4px;
143 | padding-right: 4px;
144 | border-radius: 3px;
145 | }
146 |
147 | .word-lemmas:hover {
148 | opacity: 1.0;
149 | }
150 |
151 | .word-lemmas:focus {
152 | box-shadow: none;
153 | }
154 |
155 | .more-results {
156 | color: gray;
157 | font-size: 0.9em;
158 | }
159 |
160 | /* -- transition effects -- */
161 | /* ------------------------------ */
162 | .transition-on:dir(ltr) {
163 | opacity: 0;
164 | transition: all 100ms ease-in-out;
165 | }
166 |
167 | .transition-on:hover {
168 | opacity: 1;
169 | transition: all 250ms ease-in-out;
170 | }
171 |
172 |
173 | /* -- no-word-view -- */
174 | /* ------------------------------ */
175 | .no-word-view > entry > image.right {
176 | padding-right: 4px;
177 | }
178 |
179 | .no-word-view > entry {
180 | font-size: large;
181 | }
182 |
183 | scrolledwindow > undershoot.top {
184 | background-blend-mode: normal;
185 | background-clip: border-box;
186 | background-color: rgba(0,0,0,0);
187 | background-image: linear-gradient(@theme_bg_color 0, alpha(@theme_bg_color, 0) 50%);
188 | background-origin: padding-box;
189 | background-position: left top;
190 | background-repeat: repeat;
191 | background-size: auto;
192 | }
193 |
194 | scrolledwindow > undershoot.bottom {
195 | background-blend-mode: normal;
196 | background-clip: border-box;
197 | background-color: rgba(0,0,0,0);
198 | background-image: linear-gradient(alpha(@theme_bg_color, 0) 50%, @theme_bg_color 100%);
199 | background-origin: padding-box;
200 | background-position: left top;
201 | background-repeat: repeat;
202 | background-size: auto;
203 | }
204 |
205 | button#speak:hover {
206 | color: @accent_color_500;
207 | }
208 |
--------------------------------------------------------------------------------
/data/com.github.hezral.quickword.appdata.xml.in:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | com.github.hezral.quickword
5 | CC0
6 | GPL-3.0+
7 | QuickWord
8 | Quick and easy word lookups
9 | Adi Hezral
10 |
11 | QuickWord can be used by manual lookup and via a keyboard shortcut to open it on the fly.
12 | Select a word and hit the ⌘ + Ctrl + D shortcut to get the word lookup.
13 | Features include:
14 |
15 | - On the fly word lookup
16 | - Works offline (first run will require internet to download dictionary data)
17 | - Copy definitions and examples to clipboard with just a click
18 | - Word definitions
19 | - Examples of word sentences
20 | - Synonyms
21 | - Explore synonyms from each word
22 |
23 |
24 |
25 |
26 | com.github.hezral.quickword
27 |
28 |
29 | https://github.com/hezral/quickword
30 | https://github.com/hezral/quickword/issues
31 | https://github.com/hezral/quickword/issues/labels/bug
32 |
33 |
34 |
35 | Manual word lookup dark theme
36 | https://github.com/hezral/quickword/blob/master/data/screenshot-01.png?raw=true
37 |
38 |
39 | Word lookup results
40 | https://github.com/hezral/quickword/blob/master/data/screenshot-02.png?raw=true
41 |
42 |
43 | Settings
44 | https://github.com/hezral/quickword/blob/master/data/screenshot-03.png?raw=true
45 |
46 |
47 |
48 |
49 | none
50 | none
51 | none
52 | none
53 | none
54 | none
55 | none
56 | none
57 | none
58 | none
59 | none
60 | none
61 | none
62 | none
63 | none
64 | none
65 | none
66 | none
67 | none
68 | none
69 | none
70 | mild
71 | none
72 | none
73 | none
74 | none
75 | none
76 |
77 |
78 |
79 |
80 |
81 | Add option to always show close button, fix #13 issue
82 | Fixed and improved appdata info
83 |
84 |
85 |
86 |
87 |
88 | #D9262C
89 | #FFFFFF
90 | 2.00
91 | pk_live_51HUnWtAIOwvlC6vLMv119RvqqsN3PWrr9CpRuQ8Jzn2pX5MPXL9oDHfdRdIzWIKr9TS39PCqQkf4QrtEFxN5Dquf00Ubl3tXzb
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/data/com.github.hezral.quickword.desktop.in:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Quickword
3 | Comment=Quick and easy word lookup
4 | Exec=com.github.hezral.quickword
5 | Icon=com.github.hezral.quickword
6 | Terminal=false
7 | Type=Application
8 | Categories=Utilities;Dictionary;Office;
9 | StartupNotify=true
10 |
--------------------------------------------------------------------------------
/data/com.github.hezral.quickword.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | false
7 | If the dark Gtk stylesheet should be used
8 | If the dark Gtk stylesheet should be used
9 |
10 |
11 | true
12 | Whether this is the first time that QuickWord has opened
13 | Used to determine whether or not to perform initial install steps
14 |
15 |
16 | false
17 | Enable/Disable this behaviour
18 | Close on focus out of the QuickWord app window
19 |
20 |
21 | false
22 | Enable/Disable this behaviour
23 | Display QuickWord app window on all workspaces
24 |
25 |
26 | false
27 | Enable/Disable this behaviour
28 | Display QuickWord app window on top of all windows
29 |
30 |
31 | false
32 | Show window close button
33 | Show window close button
34 |
35 |
36 | false
37 | Follow OS Appearance Style
38 | Follow OS Appearance Style
39 |
40 |
41 | false
42 | Always show close button
43 | Always show close button
44 |
45 |
46 | false
47 | Close to exit or run in background
48 | Close to exit or run in background
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/data/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hezral/quickword/d5da6df6f5416852bd33204d14bd450d40ad796b/data/demo.gif
--------------------------------------------------------------------------------
/data/icons/128.svg:
--------------------------------------------------------------------------------
1 |
63 |
--------------------------------------------------------------------------------
/data/icons/16-symbolic.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/data/icons/16.svg:
--------------------------------------------------------------------------------
1 |
57 |
--------------------------------------------------------------------------------
/data/icons/96.svg:
--------------------------------------------------------------------------------
1 |
63 |
--------------------------------------------------------------------------------
/data/icons/com.github.hezral.quickword-coffee.svg:
--------------------------------------------------------------------------------
1 |
53 |
--------------------------------------------------------------------------------
/data/icons/com.github.hezral.quickword-left.svg:
--------------------------------------------------------------------------------
1 |
39 |
--------------------------------------------------------------------------------
/data/icons/com.github.hezral.quickword-right.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/data/icons/com.github.hezral.quickword-symbolic.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/data/icons/prep_icons.py:
--------------------------------------------------------------------------------
1 | import pathlib, os, shutil
2 | from os import path
3 | from subprocess import call
4 |
5 | # Base application info
6 | base_rdnn = 'com.github.hezral'
7 | app_name = 'quickword'
8 | app_id = base_rdnn + '.' + app_name
9 | app_url = 'https://github.com/hezral/' + app_name
10 | saythanks_url = 'https://saythanks.io/to/adihezral%40gmail.com'
11 |
12 | # Setup file paths for data files
13 | prefix = '/usr'
14 | prefix_data = path.join(prefix, 'share')
15 | install_path = path.join(prefix_data, app_id)
16 | icon_path = 'icons/hicolor'
17 | icon_sizes = ['16','24','32','48','64','128']
18 |
19 | # Setup install data list
20 |
21 |
22 | # Add icon data files to install data list
23 | for size in icon_sizes:
24 | prefix_size = size + 'x' + size
25 | prefix_size2x = size + 'x' + size + '@2'
26 | icon_dir = path.join(prefix_data, icon_path, prefix_size, 'apps')
27 | icon_dir2x = path.join(prefix_data, icon_path, prefix_size2x, 'apps')
28 | icon_file = 'data/icons/' + size + '.svg'
29 | if not os.path.exists('data/icons/' + size):
30 | os.makedirs('data/icons/' + size)
31 | new_icon_file = path.join('data/icons/', size, (app_id + '.svg'))
32 | shutil.copyfile(icon_file, new_icon_file)
33 | file = (icon_dir, [new_icon_file])
34 | file2x = (icon_dir2x, [new_icon_file])
35 | print(file)
36 | print(file2x)
--------------------------------------------------------------------------------
/data/logo_type.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hezral/quickword/d5da6df6f5416852bd33204d14bd450d40ad796b/data/logo_type.png
--------------------------------------------------------------------------------
/data/meson.build:
--------------------------------------------------------------------------------
1 | desktop_file = i18n.merge_file(
2 | input: 'com.github.hezral.quickword.desktop.in',
3 | output: 'com.github.hezral.quickword.desktop',
4 | type: 'desktop',
5 | po_dir: '../po',
6 | install: true,
7 | install_dir: join_paths(get_option('datadir'), 'applications')
8 | )
9 |
10 | desktop_utils = find_program('desktop-file-validate', required: false)
11 | if desktop_utils.found()
12 | test('Validate desktop file', desktop_utils,
13 | args: [desktop_file]
14 | )
15 | endif
16 |
17 | appstream_file = i18n.merge_file(
18 | input: 'com.github.hezral.quickword.appdata.xml.in',
19 | output: 'com.github.hezral.quickword.appdata.xml',
20 | po_dir: '../po',
21 | install: true,
22 | install_dir: join_paths(get_option('datadir'), 'metainfo')
23 | )
24 |
25 | appstream_util = find_program('appstream-util', required: false)
26 | if appstream_util.found()
27 | test('Validate appstream file', appstream_util,
28 | args: ['validate', appstream_file]
29 | )
30 | endif
31 |
32 | install_data('com.github.hezral.quickword.gschema.xml',
33 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
34 | )
35 |
36 | compile_schemas = find_program('glib-compile-schemas', required: false)
37 | if compile_schemas.found()
38 | test('Validate schema file', compile_schemas,
39 | args: ['--strict', '--dry-run', meson.current_source_dir()]
40 | )
41 | endif
42 |
43 | install_data('application.css',
44 | install_dir: join_paths(pkgdatadir, project_short_name, 'data')
45 | )
46 |
47 | # install_data(
48 | # join_paths('icons', '128', meson.project_name() + '.svg'),
49 | # install_dir: join_paths(get_option ('datadir'), 'icons', 'hicolor', 'scalable', 'apps'),
50 | # rename: meson.project_name() + '.svg'
51 | # )
52 |
53 | install_data(
54 | join_paths('icons', meson.project_name() + '-' + 'symbolic' + '.svg'),
55 | install_dir: join_paths(get_option ('datadir'), 'icons', 'hicolor', 'symbolic', 'apps'),
56 | )
57 |
58 | other_icon_sources = [
59 | join_paths('icons', meson.project_name() + '-' + 'left' + '.svg'),
60 | join_paths('icons', meson.project_name() + '-' + 'right' + '.svg'),
61 | join_paths('icons', meson.project_name() + '-' + 'symbolic' + '.svg'),
62 | join_paths('icons', meson.project_name() + '-' + 'coffee' + '.svg'),
63 | ]
64 | iconsdir = join_paths(pkgdatadir, project_short_name, 'data', 'icons')
65 | install_data(other_icon_sources, install_dir: iconsdir)
66 |
67 | icon_sizes = ['16', '24', '32', '48', '64', '128']
68 | foreach i : icon_sizes
69 | install_data(
70 | join_paths('icons', i + '.svg'),
71 | rename: meson.project_name() + '.svg',
72 | install_dir: join_paths(get_option ('datadir'), 'icons', 'hicolor', i + 'x' + i, 'apps')
73 | )
74 | install_data(
75 | join_paths('icons', i + '.svg'),
76 | rename: meson.project_name() + '.svg',
77 | install_dir: join_paths(get_option ('datadir'), 'icons', 'hicolor', i + 'x' + i + '@2', 'apps')
78 | )
79 | endforeach
--------------------------------------------------------------------------------
/data/screenshot-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hezral/quickword/d5da6df6f5416852bd33204d14bd450d40ad796b/data/screenshot-01.png
--------------------------------------------------------------------------------
/data/screenshot-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hezral/quickword/d5da6df6f5416852bd33204d14bd450d40ad796b/data/screenshot-02.png
--------------------------------------------------------------------------------
/data/screenshot-03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hezral/quickword/d5da6df6f5416852bd33204d14bd450d40ad796b/data/screenshot-03.png
--------------------------------------------------------------------------------
/debian/changelog:
--------------------------------------------------------------------------------
1 | com.github.hezral.quickword (2.0.1) precise; urgency=low
2 |
3 | * Small bug fix and improvement release
4 |
5 | -- Adi Hezral Fri, 12 Nov 2021 00:00:00 +0800
6 |
--------------------------------------------------------------------------------
/debian/compat:
--------------------------------------------------------------------------------
1 | 9
--------------------------------------------------------------------------------
/debian/control:
--------------------------------------------------------------------------------
1 | Source: com.github.hezral.quickword
2 | Section: utils
3 | Priority: optional
4 | Maintainer: Adi Hezral
5 | Build-Depends: debhelper (>= 10),
6 | dh-python,
7 | python3,
8 | meson,
9 | libgranite-dev
10 | X-Python3-Version: >= 3.6.9
11 | Standards-Version: 3.9.3
12 |
13 | Package: com.github.hezral.quickword
14 | Architecture: any
15 | Depends: ${python3:Depends},
16 | ${misc:Depends},
17 | python3,
18 | python3-gi,
19 | python3-xlib
20 | python3-nltk,
21 | espeak,
22 | gir1.2-granite-1.0,
23 | libgranite5 | libgranite6
24 | Description: Quick and easy word lookup on the fly and offline
25 | QuickWord can be used by manual lookup and via a keyboard shortcut to open it
26 | on the fly.
27 | .
28 | Features:
29 | • On the fly word lookup
30 | • Works offline (first run will require internet to download dictionary data)
31 | • Copy definitions and examples to clipboard with just a click
32 | • Word definitions
33 | • Examples of word sentences
34 | • Explore synonyms from each word
35 |
--------------------------------------------------------------------------------
/debian/copyright:
--------------------------------------------------------------------------------
1 | Format: http://dep.debian.net/deps/dep5
2 | Upstream-Name: quickword
3 | Source: https://github.com/hezral/quickword
4 |
5 | Files: *
6 | Copyright: 2020 Adi Hezral
7 | License: GPL-3.0+
8 |
9 | License: GPL-3.0+
10 | This program is free software: you can redistribute it and/or modify
11 | it under the terms of the GNU General Public License as published by
12 | the Free Software Foundation, either version 3 of the License, or
13 | (at your option) any later version.
14 | .
15 | This package is distributed in the hope that it will be useful,
16 | but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 | GNU General Public License for more details.
19 | .
20 | You should have received a copy of the GNU General Public License
21 | along with this program. If not, see .
22 | .
23 | On Debian systems, the complete text of the GNU General
24 | Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
25 |
--------------------------------------------------------------------------------
/debian/rules:
--------------------------------------------------------------------------------
1 | #!/usr/bin/make -f
2 | # -*- makefile -*-
3 | # Sample debian/rules that uses debhelper.
4 | # This file was originally written by Joey Hess and Craig Small.
5 | # As a special exception, when this file is copied by dh-make into a
6 | # dh-make output file, you may use that output file without restriction.
7 | # This special exception was added by Craig Small in version 0.37 of dh-make.
8 |
9 | # Uncomment this to turn on verbose mode.
10 | #export DH_VERBOSE=1
11 |
12 | %:
13 | dh $@ --with python3 --buildsystem=pybuild
--------------------------------------------------------------------------------
/debian/source/format:
--------------------------------------------------------------------------------
1 | 3.0 (native)
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project('com.github.hezral.quickword',
2 | version: '2.0.1',
3 | meson_version: '>= 0.50.0',
4 | default_options: [ 'warning_level=2',
5 | ],
6 | )
7 |
8 | i18n = import('i18n')
9 |
10 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
11 | rdnn = meson.project_name().split('.')
12 | project_domain = '.'.join([rdnn[0],rdnn[1],rdnn[2]])
13 | project_short_name = rdnn[3]
14 |
15 | subdir('data')
16 | subdir('src')
17 | subdir('po')
18 |
19 | meson.add_install_script('build-aux/meson/postinstall.py')
20 |
--------------------------------------------------------------------------------
/po/LINGUAS:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hezral/quickword/d5da6df6f5416852bd33204d14bd450d40ad796b/po/LINGUAS
--------------------------------------------------------------------------------
/po/POTFILES:
--------------------------------------------------------------------------------
1 | data/com.github.hezral.quickword.desktop.in
2 | data/com.github.hezral.quickword.appdata.xml.in
3 | data/com.github.hezral.quickword.gschema.xml
4 |
5 |
--------------------------------------------------------------------------------
/po/meson.build:
--------------------------------------------------------------------------------
1 | i18n.gettext('quickword', preset: 'glib')
2 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hezral/quickword/d5da6df6f5416852bd33204d14bd450d40ad796b/src/__init__.py
--------------------------------------------------------------------------------
/src/active_window_manager.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 |
5 | # pylint: disable=unused-import
6 | from contextlib import contextmanager
7 | from typing import Any, Dict, Optional, Tuple, Union # noqa
8 |
9 | from Xlib import X
10 | from Xlib.display import Display
11 | from Xlib.error import XError
12 | from Xlib.xobject.drawable import Window
13 | from Xlib.protocol.rq import Event
14 |
15 | import threading
16 |
17 | import gi
18 | from gi.repository import GLib
19 |
20 | from datetime import datetime
21 |
22 | class ActiveWindowManager():
23 | # Based on code by Stephan Sokolow
24 | # Source: https://gist.github.com/ssokolow/e7c9aae63fb7973e4d64cff969a78ae8
25 | # Modified by hezral to add _get_window_class_name function
26 |
27 | """python-xlib example which reacts to changing the active window/title.
28 |
29 | Requires:
30 | - Python
31 | - python-xlib
32 |
33 | Tested with Python 2.x because my Kubuntu 14.04 doesn't come with python-xlib
34 | for Python 3.x.
35 |
36 | Design:
37 | -------
38 |
39 | Any modern window manager that isn't horrendously broken maintains an X11
40 | property on the root window named _NET_ACTIVE_WINDOW.
41 |
42 | Any modern application toolkit presents the window title via a property
43 | named _NET_WM_NAME.
44 |
45 | This listens for changes to both of them and then hides duplicate events
46 | so it only reacts to title changes once.
47 |
48 | Known Bugs:
49 | -----------
50 |
51 | - Under some circumstances, I observed that the first window creation and last
52 | window deletion on on an empty desktop (ie. not even a taskbar/panel) would
53 | go ignored when using this test setup:
54 |
55 | Xephyr :3 &
56 | DISPLAY=:3 openbox &
57 | DISPLAY=:3 python3 x11_watch_active_window.py
58 |
59 | # ...and then launch one or more of these in other terminals
60 | DISPLAY=:3 leafpad
61 | """
62 |
63 | stop_thread = False
64 | id_thread = None
65 | callback = None
66 |
67 | def __init__(self, gtk_application=None):
68 | super().__init__()
69 |
70 | self.app = gtk_application
71 |
72 | # Connect to the X server and get the root window
73 | self.disp = Display()
74 | self.root = self.disp.screen().root
75 |
76 | # Prepare the property names we use so they can be fed into X11 APIs
77 | self.NET_ACTIVE_WINDOW = self.disp.intern_atom('_NET_ACTIVE_WINDOW')
78 | self.NET_WM_NAME = self.disp.intern_atom('_NET_WM_NAME') # UTF-8
79 | self.WM_NAME = self.disp.intern_atom('WM_NAME') # Legacy encoding
80 | self.WM_CLASS = self.disp.intern_atom('WM_CLASS')
81 |
82 | self.last_seen = {'xid': None, 'title': None} # type: Dict[str, Any]
83 |
84 | def _run(self, callback):
85 |
86 | self.callback = callback
87 | self.stop_thread = False
88 |
89 | def init_manager():
90 | # Listen for _NET_ACTIVE_WINDOW changes
91 | self.root.change_attributes(event_mask=X.PropertyChangeMask)
92 |
93 | # Prime last_seen with whatever window was active when we started this
94 | self.get_window_name(self.get_active_window()[0])
95 | self.handle_change(self.last_seen)
96 |
97 | while True: # next_event() sleeps until we get an event
98 | self.handle_xevent(self.disp.next_event())
99 | if self.stop_thread:
100 | print(datetime.now(), "active_window_manager stopped")
101 | break
102 |
103 | self.thread = threading.Thread(target=init_manager)
104 | self.thread.daemon = True
105 | self.thread.start()
106 | print(datetime.now(), "active_window_manager started")
107 |
108 | def _stop(self):
109 | self.stop_thread = True
110 |
111 | @contextmanager
112 | def window_obj(self, win_id: Optional[int]) -> Window:
113 | """Simplify dealing with BadWindow (make it either valid or None)"""
114 | window_obj = None
115 | if win_id:
116 | try:
117 | window_obj = self.disp.create_resource_object('window', win_id)
118 | except XError:
119 | pass
120 | yield window_obj
121 |
122 | def get_active_window(self) -> Tuple[Optional[int], bool]:
123 | """Return a (window_obj, focus_has_changed) tuple for the active window."""
124 | response = self.root.get_full_property(self.NET_ACTIVE_WINDOW, X.AnyPropertyType)
125 | if not response:
126 | return None, False
127 | win_id = response.value[0]
128 |
129 | focus_changed = (win_id != self.last_seen['xid'])
130 | if focus_changed:
131 | with self.window_obj(self.last_seen['xid']) as old_win:
132 | if old_win:
133 | old_win.change_attributes(event_mask=X.NoEventMask)
134 |
135 | self.last_seen['xid'] = win_id
136 | with self.window_obj(win_id) as new_win:
137 | if new_win:
138 | new_win.change_attributes(event_mask=X.PropertyChangeMask)
139 |
140 | return win_id, focus_changed
141 |
142 | def _get_window_name_inner(self, win_obj: Window) -> str:
143 | """Simplify dealing with _NET_WM_NAME (UTF-8) vs. WM_NAME (legacy)"""
144 | for atom in (self.NET_WM_NAME, self.WM_NAME):
145 | try:
146 | window_name = win_obj.get_full_property(atom, 0)
147 | except UnicodeDecodeError: # Apparently a Debian distro package bug
148 | title = ""
149 | else:
150 | if window_name:
151 | win_name = window_name.value # type: Union[str, bytes]
152 | if isinstance(win_name, bytes):
153 | # Apparently COMPOUND_TEXT is so arcane that this is how
154 | # tools like xprop deal with receiving it these days
155 | win_name = win_name.decode('latin1', 'replace')
156 | return win_name
157 | else:
158 | title = ""
159 |
160 | return "{} (XID: {})".format(title, win_obj.id)
161 |
162 | def _get_window_class_name(self, win_obj: Window) -> str:
163 | """SReturn window class name"""
164 | try:
165 | window_name = win_obj.get_full_property(self.WM_CLASS, 0)
166 | except UnicodeDecodeError: # Apparently a Debian distro package bug
167 | title = ""
168 | else:
169 | if window_name:
170 | win_class_name = window_name.value # type: Union[str, bytes]
171 | if isinstance(win_class_name, bytes):
172 | # Apparently COMPOUND_TEXT is so arcane that this is how
173 | # tools like xprop deal with receiving it these days
174 | win_class_name = win_class_name.replace(b'\x00',b' ').decode("utf-8").lower()
175 | return win_class_name
176 | else:
177 | title = ""
178 |
179 | return "{} (XID: {})".format(title, win_obj.id)
180 |
181 | def get_window_name(self, win_id: Optional[int]) -> Tuple[Optional[str], bool]:
182 | """
183 | Look up the window class name for a given X11 window ID
184 | retrofitted to provide window class name instead of window title
185 | """
186 | if not win_id:
187 | self.last_seen['title'] = None
188 | return self.last_seen['title'], True
189 |
190 | title_changed = False
191 | with self.window_obj(win_id) as wobj:
192 | if wobj:
193 | try:
194 | win_title = self._get_window_class_name(wobj)
195 | except XError:
196 | pass
197 | else:
198 | title_changed = (win_title != self.last_seen['title'])
199 | self.last_seen['title'] = win_title
200 |
201 | return self.last_seen['title'], title_changed
202 |
203 | def handle_xevent(self, event: Event):
204 | """Handler for X events which ignores anything but focus/title change"""
205 | if event.type != X.PropertyNotify:
206 | return
207 |
208 | changed = False
209 | if event.atom == self.NET_ACTIVE_WINDOW:
210 | if self.get_active_window()[1]:
211 | self.get_window_name(self.last_seen['xid']) # Rely on the side-effects
212 | changed = True
213 | elif event.atom in (self.NET_WM_NAME, self.WM_NAME):
214 | changed = changed or self.get_window_name(self.last_seen['xid'])[1]
215 |
216 | if changed:
217 | self.handle_change(self.last_seen)
218 |
219 | def handle_change(self, new_state: dict):
220 | """Replace this with whatever you want to actually do"""
221 | GLib.idle_add(self.callback, new_state['title'])
222 |
--------------------------------------------------------------------------------
/src/clipboard_manager.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import gi
5 | gi.require_version('Gtk', '3.0')
6 | from gi.repository import Gtk, Gdk
7 |
8 |
9 | #------------------CLASS-SEPARATOR------------------#
10 |
11 |
12 | class Clipboard():
13 | def __init__(self, atom_type):
14 |
15 | # create clipboard
16 | self.clipboard = Gtk.Clipboard.get(atom_type)
17 |
18 | # target type to listen for
19 | self.text_target = Gdk.Atom.intern('text/plain', False)
20 |
21 |
22 | #------------------CLASS-SEPARATOR------------------#
23 |
24 |
25 | class ClipboardListener(Clipboard):
26 | def __init__(self, *args, **kwargs):
27 | super().__init__(atom_type=Gdk.SELECTION_PRIMARY, *args, **kwargs)
28 |
29 | def copy_selected_text(self, clipboard=None, event=None):
30 | if self.clipboard.wait_is_target_available(self.text_target):
31 | content = self.clipboard.wait_for_text()
32 |
33 | # remove multiple lines and only return first line
34 | if "\n" in content:
35 | content = content.split("\n")[0]
36 |
37 | # lowercase the word
38 | content = content.lower()
39 |
40 | # remove spaces in front and back
41 | content = content.strip()
42 |
43 | # remove special characters in front of word
44 | if len(content) > 0:
45 | FirstContainsSpecialChars = any(not c.isalnum() for c in content[0])
46 | LastContainsSpecialChars = any(not c.isalnum() for c in content[len(content)-1])
47 |
48 | if FirstContainsSpecialChars:
49 | content = content[1:]
50 | if LastContainsSpecialChars:
51 | content = content[0:-1]
52 |
53 | # don't return empty lines
54 | if len(content) > 0:
55 | valid = True
56 | return content
57 | else:
58 | content = None
59 | valid = False
60 | return content
61 |
62 |
63 | #------------------CLASS-SEPARATOR------------------#
64 |
65 |
66 | class ClipboardPaste(Clipboard):
67 | def __init__(self, *args, **kwargs):
68 | super().__init__(atom_type=Gdk.SELECTION_CLIPBOARD, *args, **kwargs)
69 |
70 | def copy_to_clipboard(self, text_to_copy):
71 | self.clipboard.set_text(text_to_copy, -1)
72 |
--------------------------------------------------------------------------------
/src/data_manager.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import os
5 |
6 |
7 | import gi
8 | gi.require_version('Gtk', '3.0')
9 | from gi.repository import GLib, Gtk
10 |
11 | import threading
12 | import time
13 |
14 | from nltk import data, downloader
15 |
16 | DATA_IDS = ('wordnet',)
17 |
18 |
19 | #------------------CLASS-SEPARATOR------------------#
20 |
21 |
22 | class DataManager():
23 | def __init__(self, application_id="com.github.hezral.quickword", *args, **kwargs):
24 |
25 | self.nltk_data_path = os.path.join(GLib.get_user_data_dir(), application_id, 'nltk_data')
26 |
27 | # setup paths
28 | data.path = [self.nltk_data_path]
29 | downloader.download_dir = self.nltk_data_path
30 |
31 | # check if data dir exist
32 | if not os.path.exists(self.nltk_data_path):
33 | os.makedirs(self.nltk_data_path)
34 |
35 | def update_data(self, callback=None):
36 | # check if any data is stale and update it
37 | stale = 0
38 | for id in DATA_IDS:
39 | if downloader.Downloader().is_stale(id, self.nltk_data_path):
40 | stale += 1
41 | GLib.idle_add(callback, "Updating", id + " has an update")
42 | time.sleep(0.5)
43 | else:
44 | GLib.idle_add(callback, "No Updates", id + " is up-to-date")
45 | if stale > 0:
46 | GLib.idle_add(callback, "Updates Available", "There are " + str(stale) + "data updates available for download")
47 | time.sleep(0.5)
48 | downloader.Downloader().update(quiet=False, prefix=self.nltk_data_path)
49 | time.sleep(0.5)
50 | GLib.idle_add(callback, "Completed", "All data has been updated.")
51 | else:
52 | time.sleep(0.5)
53 | GLib.idle_add(callback, "No Updates", "All data is up-to-date.")
54 |
55 | def download_data(self, callback=None):
56 | # download data if not installed
57 | for id in DATA_IDS:
58 | if not downloader.Downloader().is_installed(id, self.nltk_data_path):
59 | GLib.idle_add(callback, "Downloading...", "Downloading " + id)
60 | time.sleep(0.5)
61 | downloader.Downloader().download(id)
62 | time.sleep(0.5)
63 | GLib.idle_add(callback, "Downloading", id + " data has been downloaded.")
64 | time.sleep(0.5)
65 | GLib.idle_add(callback, "Completed", "All data has been downloaded.")
66 | else:
67 | time.sleep(0.5)
68 | GLib.idle_add(callback, "Downloaded", "All data has already been downloaded.")
69 |
70 |
71 |
72 | def run_func(self, runname=None, callback=None):
73 | if runname == "download":
74 | target = self.download_data
75 | else:
76 | target = self.update_data
77 | thread = threading.Thread(target=target, args=(callback,))
78 | thread.daemon = True
79 | thread.start()
80 |
81 |
82 |
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import sys, os
5 |
6 | import gi
7 | gi.require_version('Gtk', '3.0')
8 | gi.require_version('Granite', '1.0')
9 | from gi.repository import Gtk, Gio, Gdk, Granite
10 |
11 | from .main_window import QuickWordWindow
12 | from .clipboard_manager import ClipboardListener, ClipboardPaste
13 | from .word_lookup import WordLookup
14 | from .active_window_manager import ActiveWindowManager
15 | from .utils import HelperUtils
16 |
17 | from datetime import datetime
18 |
19 | #------------------CLASS-SEPARATOR------------------#
20 |
21 | class QuickWordApp(Gtk.Application):
22 |
23 | app_id = "com.github.hezral.quickword"
24 | gtk_settings = Gtk.Settings().get_default()
25 | gio_settings = Gio.Settings(schema_id=app_id)
26 | granite_settings = Granite.Settings.get_default()
27 | clipboard_listener = ClipboardListener()
28 | clipboard_paste = ClipboardPaste()
29 | utils = HelperUtils()
30 | window_manager = None
31 | window = None
32 | lookup_word = None
33 | word_data = None
34 | running = False
35 |
36 | def __init__(self, *args, **kwargs):
37 | super().__init__(*args, **kwargs)
38 |
39 | self.props.application_id = self.app_id
40 | self.window_manager = ActiveWindowManager(gtk_application=self)
41 | self.first_run = self.gio_settings.get_value("first-run")
42 |
43 | # initialize word lookup
44 | self._word_lookup = WordLookup(application_id=self.props.application_id)
45 | if self.first_run:
46 | self.generate_data_manager()
47 | else:
48 | self._word_lookup.get_synsets("a") # hack to load wordnet faster maybe
49 |
50 | if self.gio_settings.get_value("theme-optin"):
51 | prefers_color_scheme = self.granite_settings.get_prefers_color_scheme()
52 | self.gtk_settings.set_property("gtk-application-prefer-dark-theme", prefers_color_scheme)
53 | self.granite_settings.connect("notify::prefers-color-scheme", self.on_prefers_color_scheme)
54 |
55 | def do_startup(self):
56 | Gtk.Application.do_startup(self)
57 |
58 | # setup quiting app using Escape, Ctrl+Q
59 | quit_action = Gio.SimpleAction.new("quit", None)
60 | quit_action.connect("activate", self.on_quit_action)
61 | self.add_action(quit_action)
62 | self.set_accels_for_action("app.quit", ["Q", "Escape"])
63 |
64 | # set CSS provider
65 | provider = Gtk.CssProvider()
66 | provider.load_from_path(os.path.join(os.path.dirname(__file__), "data", "application.css"))
67 | Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
68 |
69 | # prepend custom path for icon theme
70 | icon_theme = Gtk.IconTheme.get_default()
71 | icon_theme.prepend_search_path(os.path.join(os.path.dirname(__file__), "data", "icons"))
72 |
73 | def do_activate(self):
74 |
75 | if self.window is None:
76 | self.window = QuickWordWindow(application=self)
77 | self.window.word_view.clipboard_paste = self.clipboard_paste
78 | self.add_window(self.window)
79 |
80 | self.window.present()
81 |
82 | # get current selected text and lookup
83 | self.on_new_word_selected()
84 |
85 | self.running = True
86 |
87 | # setup listener for new text selection
88 | self.clipboard_listener.clipboard.connect("owner-change", self.on_new_word_selected)
89 |
90 | def generate_data_manager(self):
91 | from .data_manager import DataManager
92 | self._data_manager = DataManager(application_id=self.props.application_id)
93 | return True
94 |
95 | def on_new_word_selected(self, clipboard=None, event=None):
96 | self.on_new_word_lookup(self.clipboard_listener.copy_selected_text())
97 |
98 | def on_new_word_lookup(self, word):
99 | self.lookup_word = word
100 | word_data = None
101 | if self.first_run:
102 | self.window.on_view_visible()
103 | else:
104 | if word is not None:
105 | word_data = self._word_lookup.get_synsets(word)
106 |
107 | if word_data is not None:
108 | # emit the signal to trigger content update callback
109 | self.window.emit("on-new-word-selected", word_data)
110 | return True
111 | else:
112 | # go back to no-word-view
113 | self.lookup_word = None
114 | self.window.on_manual_lookup(not_found=True)
115 | return False
116 |
117 | def on_quit_action(self, action, param):
118 | if self.window is not None:
119 | self.window.destroy()
120 |
121 | def on_prefers_color_scheme(self, *args):
122 | prefers_color_scheme = self.granite_settings.get_prefers_color_scheme()
123 | self.gtk_settings.set_property("gtk-application-prefer-dark-theme", prefers_color_scheme)
124 |
125 | def main(version):
126 | app = QuickWordApp()
127 | return app.run(sys.argv)
--------------------------------------------------------------------------------
/src/main_window.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import gi
5 | gi.require_version('Gtk', '3.0')
6 | gi.require_version('Granite', '1.0')
7 | from gi.repository import GLib, Gtk, Granite, GObject, Gdk
8 |
9 | from .settings_view import SettingsView
10 | from .noword_view import NoWordView
11 | from .word_view import WordView
12 | from .updater_view import UpdaterView
13 |
14 |
15 | class QuickWordWindow(Gtk.ApplicationWindow):
16 | def __init__(self, *args, **kwargs):
17 | super().__init__(*args, **kwargs)
18 |
19 | # Add custom signals to detect new word selected
20 | # GObject.signal_new(signal_name, type, flags, return_type, param_types)
21 | # param_types is a list example [GObject.TYPE_PYOBJECT, GObject.TYPE_STRING]
22 | GObject.signal_new("on-new-word-selected", Gtk.ApplicationWindow, GObject.SIGNAL_RUN_LAST, GObject.TYPE_BOOLEAN, [GObject.TYPE_PYOBJECT])
23 | self.connect("on-new-word-selected", self.on_new_word_selected)
24 |
25 | self.app = self.props.application
26 | self.lookup_word = self.app.lookup_word
27 |
28 | self.updater_view = UpdaterView()
29 | self.noword_view = NoWordView()
30 | self.word_view = WordView()
31 | self.settings_view = SettingsView(app=self.app)
32 |
33 | self.stack = Gtk.Stack()
34 | self.stack.props.transition_type = Gtk.StackTransitionType.CROSSFADE
35 | self.stack.add_named(self.word_view, self.word_view.get_name())
36 | self.stack.add_named(self.settings_view, self.settings_view.get_name())
37 | self.stack.add_named(self.noword_view, self.noword_view.get_name())
38 | self.stack.add_named(self.updater_view, self.updater_view.get_name())
39 |
40 | self.headerbar = self.generate_headerbar()
41 |
42 | # self.props.resizable = False #set this and window will expand and retract based on child
43 | self.title = "QuickWord"
44 | self.set_keep_above(True)
45 | self.get_style_context().add_class("rounded")
46 | self.set_size_request(-1, -1) #set width to -1 to expand and retract based on content
47 | self.props.window_position = Gtk.WindowPosition.MOUSE
48 | self.set_titlebar(self.headerbar)
49 | self.add(self.stack)
50 |
51 | self.connect("key-press-event", self.on_key_press)
52 |
53 | self.show_all()
54 | self.on_start_settings()
55 | # hide all other views to enable window to expand and retract based on content
56 | self.noword_view.hide()
57 | self.settings_view.hide()
58 | self.updater_view.hide()
59 |
60 | def on_key_press(self, widget, eventkey):
61 |
62 | if not self.noword_view.entry.props.has_focus:
63 | if Gdk.keyval_name(eventkey.keyval).lower() == "right":
64 | self.stack.set_visible_child_name("settings-view")
65 | self.on_view_visible(view="settings-view")
66 |
67 | if Gdk.keyval_name(eventkey.keyval).lower() == "left":
68 | self.on_view_visible(view="word-view")
69 |
70 | if Gdk.keyval_name(eventkey.keyval).lower() == "m":
71 | self.on_manual_lookup()
72 |
73 | if self.stack.get_visible_child_name() == "word-view":
74 | if Gdk.keyval_name(eventkey.keyval).lower() == "n":
75 | self.word_view.stack.set_visible_child_name("noun")
76 | if Gdk.keyval_name(eventkey.keyval).lower() == "a":
77 | self.word_view.stack.set_visible_child_name("adjective")
78 | if Gdk.keyval_name(eventkey.keyval).lower() == "d":
79 | self.word_view.stack.set_visible_child_name("adverb")
80 | if Gdk.keyval_name(eventkey.keyval).lower() == "v":
81 | self.word_view.stack.set_visible_child_name("verb")
82 |
83 | def on_start_settings(self):
84 | self.connect("delete-event", self.on_close_window)
85 |
86 | if self.app.gio_settings.get_value("sticky-mode"):
87 | self.stick()
88 |
89 | if not self.app.gio_settings.get_value("persistent-mode"):
90 | self.word_label.props.margin_left = 10
91 | self.headerbar.set_show_close_button(False)
92 | if self.app.window_manager is not None:
93 | self.app.window_manager._run(callback=self.on_persistent_mode)
94 | else:
95 | self.headerbar.set_show_close_button(True)
96 | self.word_label.props.margin_left = 0
97 |
98 | if self.app.gio_settings.get_value("close-button"):
99 | self.headerbar.set_show_close_button(True)
100 | self.word_label.props.margin_left = 0
101 |
102 | def on_persistent_mode(self, wm_class):
103 | if wm_class is not None:
104 | if self.app.props.application_id.split(".")[-1] not in wm_class:
105 | if self.app.running:
106 | self.on_close_window()
107 |
108 | def on_enter_word_label(self, *args):
109 | self.word_action_revealer.set_reveal_child(False)
110 | if self.stack.get_visible_child_name() == "word-view":
111 | self.edit_img_revealer.set_reveal_child(True)
112 |
113 | def on_leave_word_label(self, *args):
114 | if self.stack.get_visible_child_name() == "settings-view":
115 | pass
116 | if self.stack.get_visible_child_name() != "no-word-view" or self.stack.get_visible_child_name() != "updater-view":
117 | self.word_action_revealer.set_reveal_child(False)
118 | self.edit_img_revealer.set_reveal_child(False)
119 | if self.stack.get_visible_child_name() == "word-view":
120 | self.word_action_revealer.set_reveal_child(True)
121 | self.edit_img_revealer.set_reveal_child(False)
122 |
123 | def on_new_word_selected(self, window, word_data):
124 | self.lookup_word = word_data[0]
125 | self.word_label.props.label = word_data[0]
126 | self.word_view.on_wordlookup(data=word_data)
127 | self.word_view.show_all()
128 | self.stack.set_visible_child(self.word_view)
129 | self.props.resizable = True
130 | self.view_switch.props.active = False
131 |
132 | def on_manual_lookup(self, eventbutton=None, eventbox=None, not_found=False, *args):
133 | self.word_action_revealer.set_reveal_child(False)
134 |
135 | if self.stack.get_visible_child_name() == "word-view":
136 | if not_found:
137 | self.noword_view.message.props.label = "Word not found"
138 | else:
139 | self.noword_view.message.props.label = "Lookup a new word"
140 |
141 | # need to clear entry text since clipboard_listener will pickup the text as it will be selected on focus and cause a loop back to the word-view
142 | self.noword_view.entry.props.text = ""
143 |
144 | self.noword_view.show_all()
145 | self.noword_view.icon_overlay.grab_focus()
146 | self.stack.set_visible_child_name(name="no-word-view")
147 | self.word_label.props.label = "QuickWord"
148 | self.word_view.hide()
149 | self.settings_view.hide()
150 | self.props.resizable = False
151 | # delayed grab focus to avoid hotkey character getting inserted in entry field
152 | GLib.timeout_add(250, self.noword_view.entry.grab_focus)
153 |
154 | def generate_viewswitch(self):
155 | self.view_switch = Granite.ModeSwitch.from_icon_name("com.github.hezral.quickword-symbolic", "preferences-system-symbolic")
156 | self.view_switch.props.valign = Gtk.Align.CENTER
157 | self.view_switch.props.halign = Gtk.Align.END
158 | self.view_switch.props.hexpand = True
159 | self.view_switch.props.margin = 4
160 | self.view_switch.props.name = "view-switch"
161 | self.view_switch.get_children()[1].props.can_focus = False
162 | self.view_switch.connect_after("notify::active", self.on_view_visible)
163 | return self.view_switch
164 |
165 | def generate_headerbar(self):
166 | self.word_label = Gtk.Label("QuickWord")
167 | self.word_label.props.vexpand = True
168 | self.word_label.get_style_context().add_class("lookup-word-header")
169 |
170 | edit_img = Gtk.Image().new_from_icon_name("insert-text-symbolic", Gtk.IconSize.SMALL_TOOLBAR)
171 | edit_img.props.halign = Gtk.Align.START
172 | self.edit_img_revealer = Gtk.Revealer()
173 | self.edit_img_revealer.props.transition_type = Gtk.RevealerTransitionType.CROSSFADE
174 | self.edit_img_revealer.add(edit_img)
175 |
176 | self.pronounciation_label = Gtk.Label("")
177 | self.pronounciation_label.props.name = "pronounciation"
178 | self.pronounciation_label.props.expand = True
179 | self.pronounciation_label.props.can_focus = False
180 | self.pronounciation_label.get_style_context().add_class("pronounciation")
181 |
182 | self.speak_btn = Gtk.Button(image=Gtk.Image().new_from_icon_name("audio-volume-high-symbolic", Gtk.IconSize.SMALL_TOOLBAR))
183 | self.speak_btn.props.name = "speak"
184 | self.speak_btn.props.expand = True
185 | self.speak_btn.props.can_focus = False
186 | self.speak_btn.get_style_context().add_class("speak")
187 | self.speak_btn.connect("clicked", self.on_speak_word)
188 |
189 | self.word_actions = Gtk.Grid()
190 | self.word_actions.props.name = "word-actions"
191 | self.word_actions.props.column_spacing = 6
192 | self.word_actions.props.expand = False
193 | self.word_actions.attach(self.pronounciation_label, 0, 0, 1, 1)
194 | self.word_actions.attach(self.speak_btn, 1, 0, 1, 1)
195 |
196 | self.word_action_revealer = Gtk.Revealer()
197 | self.word_action_revealer.props.transition_type = Gtk.RevealerTransitionType.CROSSFADE
198 | self.word_action_revealer.add(self.word_actions)
199 |
200 | self.word_box = Gtk.EventBox()
201 | self.word_box.add(self.word_label)
202 | self.word_box.connect("button-press-event", self.on_manual_lookup)
203 | self.word_box.connect("enter-notify-event", self.on_enter_word_label)
204 | self.word_box.connect("leave-notify-event", self.on_leave_word_label)
205 |
206 | self.word_grid = Gtk.Grid()
207 | self.word_grid.props.column_spacing = 6
208 | self.word_grid.props.halign = Gtk.Align.START
209 | self.word_grid.props.valign = Gtk.Align.CENTER
210 | self.word_grid.attach(self.word_box, 0, 0, 1, 1)
211 | self.word_grid.attach(self.word_action_revealer, 1, 0, 1, 1)
212 | self.word_grid.attach(self.edit_img_revealer, 1, 0, 1, 1)
213 |
214 | titlebar_grid = Gtk.Grid()
215 | titlebar_grid.props.hexpand = True
216 | titlebar_grid.attach(self.word_grid, 0, 0, 1, 1)
217 | titlebar_grid.attach(self.generate_viewswitch(), 1, 0, 1, 1)
218 |
219 | headerbar = Gtk.HeaderBar()
220 | headerbar.props.custom_title = titlebar_grid
221 | headerbar.props.decoration_layout = "close:"
222 | return headerbar
223 |
224 | def on_speak_word(self, button):
225 | import subprocess
226 | from shutil import which, Error
227 | try:
228 | word = self.word_label.props.label
229 | subprocess.call(["espeak", word])
230 | print(word)
231 | except:
232 | pass
233 |
234 | def on_view_visible(self, viewswitch=None, gparam=None, view=None):
235 |
236 | def set_word_view():
237 | if self.app.lookup_word is None:
238 | self.noword_view.show_all()
239 | self.word_label.props.label = "Quickword"
240 | self.stack.set_visible_child(self.noword_view)
241 | self.word_view.hide()
242 | self.props.resizable = False
243 | else:
244 | self.word_view.show_all()
245 | self.word_label.props.label = self.lookup_word
246 | self.stack.set_visible_child(self.word_view)
247 | self.noword_view.hide()
248 | self.word_action_revealer.set_reveal_child(True)
249 | self.props.resizable = True
250 |
251 | def set_settings_view():
252 | self.word_action_revealer.set_reveal_child(False)
253 | self.settings_view.show_all()
254 | self.settings_view.on_totalwords()
255 | self.stack.set_visible_child(self.settings_view)
256 | self.word_label.props.label = "Settings"
257 | self.word_view.hide()
258 | self.noword_view.hide()
259 | self.props.resizable = False
260 |
261 | def set_updater_view():
262 | self.word_action_revealer.set_reveal_child(False)
263 | self.updater_view.show_all()
264 | self.stack.set_visible_child(self.updater_view)
265 | self.word_label.props.label = "Settings"
266 | self.word_view.hide()
267 | self.noword_view.hide()
268 | self.settings_view.hide()
269 | self.props.resizable = False
270 |
271 | if self.app.first_run:
272 | set_updater_view()
273 |
274 | if not self.app.first_run and view is None:
275 | if viewswitch is not None:
276 | if viewswitch.props.active:
277 | set_settings_view()
278 | else:
279 | set_word_view()
280 | self.settings_view.hide()
281 | else:
282 | set_word_view()
283 |
284 | if view is not None:
285 | self.stack.set_visible_child_name(view)
286 | if view == "settings-view":
287 | self.view_switch.props.active = True
288 | else:
289 | self.view_switch.props.active = False
290 |
291 | def on_close_window(self, window=None, event=None):
292 | if self.app.gio_settings.get_value("close-mode"):
293 | if window is None:
294 | self.hide()
295 | else:
296 | window.hide()
297 | return True
298 | else:
299 | self.destroy()
--------------------------------------------------------------------------------
/src/meson.build:
--------------------------------------------------------------------------------
1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
2 | moduledir = join_paths(pkgdatadir, 'quickword')
3 | gnome = import('gnome')
4 |
5 | python = import('python')
6 |
7 | conf = configuration_data()
8 | conf.set('PYTHON', python.find_installation('python3').path())
9 | conf.set('VERSION', meson.project_version())
10 | conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir')))
11 | conf.set('pkgdatadir', pkgdatadir)
12 |
13 | configure_file(
14 | input: 'quickword.in',
15 | output: 'com.github.hezral.quickword',
16 | configuration: conf,
17 | install: true,
18 | install_dir: get_option('bindir')
19 | )
20 |
21 | quickword_sources = [
22 | '__init__.py',
23 | 'utils.py',
24 | 'main.py',
25 | 'main_window.py',
26 | 'active_window_manager.py',
27 | 'clipboard_manager.py',
28 | 'data_manager.py',
29 | 'noword_view.py',
30 | 'settings_view.py',
31 | 'updater_view.py',
32 | 'word_lookup.py',
33 | 'word_view.py'
34 | ]
35 |
36 | install_data(quickword_sources, install_dir: moduledir)
37 |
--------------------------------------------------------------------------------
/src/noword_view.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import os
5 | import gi
6 | gi.require_version('Gtk', '3.0')
7 | from gi.repository import Gtk, Pango
8 |
9 |
10 | class NoWordView(Gtk.Grid):
11 | def __init__(self, *args, **kwargs):
12 | super().__init__(*args, **kwargs)
13 |
14 | #-- quickword logo --------#
15 | left_icon = Gtk.Image().new_from_icon_name("com.github.hezral.quickword-left", Gtk.IconSize.DIALOG)
16 | right_icon = Gtk.Image().new_from_icon_name("com.github.hezral.quickword-right", Gtk.IconSize.DIALOG)
17 |
18 | left_icon.set_pixel_size(96)
19 | right_icon.set_pixel_size(96)
20 |
21 | right_icon.get_style_context().add_class("quickword-icon-right")
22 |
23 | self.icon_overlay = Gtk.Overlay()
24 | self.icon_overlay.add(left_icon)
25 | self.icon_overlay.add_overlay(right_icon)
26 | self.icon_overlay.props.can_focus = True
27 | self.icon_overlay.props.focus_on_click = True
28 | # self.icon_overlay.grab_focus()
29 |
30 | #-- message header --------#
31 | self.message = Gtk.Label("No word detected")
32 | self.message.props.name = "message"
33 | self.message.props.margin_bottom = 5
34 | self.message.props.hexpand = True
35 | self.message.props.halign = Gtk.Align.CENTER
36 | self.message.props.valign = Gtk.Align.CENTER
37 | self.message.props.max_width_chars = 30
38 | self.message.props.wrap = True
39 | self.message.props.wrap_mode = Pango.WrapMode.WORD
40 | self.message.props.justify = Gtk.Justification.CENTER
41 | self.message.get_style_context().add_class("h3")
42 |
43 | #-- message header --------#
44 | self.sub_message = Gtk.Label("Select a word in any application or document\nor type a word below to get a quick word lookup")
45 | self.sub_message.props.margin_bottom = 10
46 | self.sub_message.props.hexpand = True
47 | self.sub_message.props.halign = Gtk.Align.CENTER
48 | self.sub_message.props.valign = Gtk.Align.CENTER
49 | self.sub_message.props.max_width_chars = 40
50 | self.sub_message.props.wrap = True
51 | self.sub_message.props.wrap_mode = Pango.WrapMode.WORD
52 | self.sub_message.props.justify = Gtk.Justification.CENTER
53 |
54 | #-- word entry --------#
55 | self.entry = Gtk.Entry()
56 | self.entry.set_size_request(-1, 40)
57 | self.entry.props.expand = False
58 | self.entry.props.focus_on_click = True
59 | self.entry.props.placeholder_text = "type a word like 'quick' and press enter"
60 | self.entry.props.xalign = 0.5
61 | self.entry.get_style_context().add_class("entry-word")
62 | self.entry.connect("key-press-event", self.on_entry_start)
63 | self.entry.connect("button-press-event", self.on_entry_start)
64 | self.entry.connect("activate", self.on_entry_activate)
65 | self.entry.connect("icon_press", self.on_backspace)
66 |
67 |
68 | self.props.name = 'no-word-view'
69 | self.get_style_context().add_class(self.props.name)
70 | self.set_size_request(350, -1)
71 | self.props.margin = 20
72 | self.props.margin_left = 20
73 | self.props.margin_right = 20
74 | self.props.expand = True
75 | self.props.row_spacing = 12
76 | self.props.column_spacing = 6
77 | self.props.valign = Gtk.Align.CENTER
78 | self.attach(self.icon_overlay, 0, 1, 1, 1)
79 | self.attach(self.message, 0, 2, 1, 1)
80 | self.attach(self.sub_message, 0, 3, 1, 1)
81 | self.attach(self.entry, 0, 4, 1, 1)
82 |
83 |
84 | def on_entry_start(self, entry, eventkey):
85 | entry.props.secondary_icon_name = "edit-clear-symbolic"
86 |
87 | def on_backspace(self, entry, entry_icon, eventbutton):
88 | entry.props.text = ""
89 |
90 | def on_entry_activate(self, entry):
91 | self.entry.props.secondary_icon_name = None
92 | window = self.get_toplevel()
93 |
94 | if self.entry.props.text == "":
95 | # need to reset text, bug maybe?
96 | self.entry.props.text = ""
97 | self.entry.props.placeholder_text = "need a word here 😀️"
98 | self.icon_overlay.grab_focus()
99 | else:
100 | # callback to WordLookup
101 | lookup = window.props.application.on_new_word_lookup(entry.props.text)
102 | # check if word lookup succeeded or not
103 | if lookup is False:
104 | self.message.props.label = "Word not found"
105 | self.entry.props.text = ""
106 | self.entry.props.placeholder_text = "please type a valid word 🧐️"
107 | self.icon_overlay.grab_focus()
108 | else:
109 | self.hide()
110 |
--------------------------------------------------------------------------------
/src/quickword.in:
--------------------------------------------------------------------------------
1 | #!@PYTHON@
2 |
3 | # SPDX-License-Identifier: GPL-3.0-or-later
4 | # SPDX-FileCopyrightText: 2021 Adi Hezral
5 |
6 | import os
7 | import sys
8 | import signal
9 | import gettext
10 |
11 | VERSION = '@VERSION@'
12 | pkgdatadir = '@pkgdatadir@'
13 | localedir = '@localedir@'
14 |
15 | sys.path.insert(1, pkgdatadir)
16 | signal.signal(signal.SIGINT, signal.SIG_DFL)
17 | gettext.install('quickword', localedir)
18 |
19 | if __name__ == '__main__':
20 | import gi
21 |
22 | from quickword import main
23 | print("Quickword", VERSION)
24 | sys.exit(main.main(VERSION))
25 |
--------------------------------------------------------------------------------
/src/settings_view.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import gi
5 | gi.require_version('Gtk', '3.0')
6 | gi.require_version('Granite', '1.0')
7 | from gi.repository import Gtk, Gio, Gdk, GObject, Pango, Granite
8 |
9 |
10 | class SettingsView(Gtk.Grid):
11 | def __init__(self, app, *args, **kwargs):
12 | super().__init__(*args, **kwargs)
13 |
14 | self.app = app
15 |
16 | theme_switch = SubSettings(type="switch", name="theme-switch", label="Switch between Dark/Light theme", sublabel=None, separator=False)
17 | theme_optin = SubSettings(type="checkbutton", name="theme-optin", label=None, sublabel=None, separator=True, params=("Follow system appearance style",))
18 |
19 | theme_switch.switch.bind_property("active", self.app.gtk_settings, "gtk-application-prefer-dark-theme", GObject.BindingFlags.SYNC_CREATE)
20 |
21 | self.app.granite_settings.connect("notify::prefers-color-scheme", self.on_appearance_style_change, theme_switch)
22 | theme_switch.switch.connect_after("notify::active", self.on_switch_activated)
23 | theme_optin.checkbutton.connect_after("notify::active", self.on_checkbutton_activated, theme_switch)
24 |
25 | self.app.gio_settings.bind("prefer-dark-style", theme_switch.switch, "active", Gio.SettingsBindFlags.DEFAULT)
26 | self.app.gio_settings.bind("theme-optin", theme_optin.checkbutton, "active", Gio.SettingsBindFlags.DEFAULT)
27 |
28 | close_button = SubSettings(type="switch", name="close-button", label="Show close button", sublabel="Always show close button",separator=True)
29 | close_button.switch.connect_after("notify::active", self.on_switch_activated)
30 | self.app.gio_settings.bind("close-button", close_button.switch, "active", Gio.SettingsBindFlags.DEFAULT)
31 |
32 | persistent_mode = SubSettings(type="switch", name="persistent-mode", label="Persistent mode", sublabel="Stays open and updates as text changes",separator=True)
33 | persistent_mode.switch.connect_after("notify::active", self.on_switch_activated, close_button)
34 | self.app.gio_settings.bind("persistent-mode", persistent_mode.switch, "active", Gio.SettingsBindFlags.DEFAULT)
35 |
36 | close_mode = SubSettings(type="switch", name="close-mode", label="Background mode", sublabel="Close window and run-in-background",separator=True)
37 | close_mode.switch.connect_after("notify::active", self.on_switch_activated)
38 | self.app.gio_settings.bind("close-mode", close_mode.switch, "active", Gio.SettingsBindFlags.DEFAULT)
39 |
40 | sticky_mode = SubSettings(type="switch", name="sticky-mode", label="Sticky mode", sublabel="Window is displayed on all workspaces",separator=False)
41 | sticky_mode.switch.connect_after("notify::active", self.on_switch_activated)
42 | self.app.gio_settings.bind("sticky-mode", sticky_mode.switch, "active", Gio.SettingsBindFlags.DEFAULT)
43 |
44 | display_behaviour = SettingsGroup("Display & Behaviour", (theme_switch, theme_optin, persistent_mode, close_button, close_mode, sticky_mode))
45 |
46 | buyme_coffee = SubSettings(type="button", name="buy-me-coffee", label="Show Support", sublabel="Thanks for supporting me!", separator=False, params=("Coffee Time", Gtk.Image().new_from_icon_name("com.github.hezral.quickword-coffee", Gtk.IconSize.LARGE_TOOLBAR), ))
47 | buyme_coffee.button.connect("clicked", self.on_button_clicked)
48 |
49 | check_updates = SubSettings(type="button", name="check-updates", label="NA", sublabel=None, separator=True, params=("Check Updates", Gtk.Image().new_from_icon_name("software-update-available", Gtk.IconSize.LARGE_TOOLBAR), ))
50 | check_updates.button.connect("clicked", self.on_button_clicked)
51 |
52 | others = SettingsGroup("Others", (check_updates, buyme_coffee, ))
53 |
54 | self.props.name = "settings-view"
55 | self.get_style_context().add_class(self.props.name)
56 | self.props.expand = True
57 | self.props.margin = 20
58 | self.props.margin_top = 10
59 | self.props.row_spacing = 10
60 | self.props.column_spacing = 6
61 | self.attach(display_behaviour, 0, 0, 1, 1)
62 | self.attach(others, 0, 1, 1, 1)
63 |
64 | def on_button_clicked(self, button, params=None):
65 | name = button.get_name()
66 | if name == "check-updates":
67 | self.on_check_update()
68 | if name == "buy-me-coffee":
69 | Gtk.show_uri_on_window(None, "https://www.buymeacoffee.com/hezral", Gdk.CURRENT_TIME)
70 |
71 | def on_check_update(self):
72 | stack = self.get_parent()
73 | window = stack.get_parent()
74 |
75 | window.updater_view.show_all()
76 | window.stack.set_visible_child_name("updater-view")
77 | window.word_label.props.label = "Updater"
78 | window.settings_view.hide()
79 |
80 | def on_totalwords(self):
81 | stack = self.get_parent()
82 | window = stack.get_parent()
83 | app = window.props.application
84 | others_settinggroup = [child for child in self.get_children() if child.get_name() == "Others"][0]
85 | others_settinggroup_frame = [child for child in others_settinggroup.get_children() if isinstance(child, Gtk.Frame)][0]
86 | check_updates_subsetting = [child for child in others_settinggroup_frame.get_children()[0].get_children() if child.get_name() == "check-updates"][0]
87 | # print(check_updates_subsetting.get_children())
88 |
89 | if check_updates_subsetting.label_text.props.label == "NA":
90 | check_updates_subsetting.label_text.props.label = "Total words available: " + str(app._word_lookup.get_totalwords())
91 |
92 | def generate_separator(self):
93 | separator = Gtk.Separator()
94 | separator.props.hexpand = True
95 | separator.props.valign = Gtk.Align.CENTER
96 | return separator
97 |
98 | def on_switch_activated(self, switch, gparam, widget=None):
99 | name = switch.get_name()
100 | main_window = self.get_toplevel()
101 |
102 | if self.is_visible():
103 | # stack = self.get_parent()
104 | # window = stack.get_parent()
105 | if name == "persistent-mode":
106 | close_button = widget
107 | if switch.get_active():
108 | self.app.window_manager._stop()
109 | main_window.set_keep_above(True) # manually trigger a window manager event to stop the thread
110 | main_window.headerbar.set_show_close_button(True)
111 | main_window.word_label.props.margin_left = 0
112 | # if self.app.gio_settings.get_value("close-button"):
113 | # close_button.props.sensitive = True
114 | else:
115 | self.app.window_manager._run(callback=main_window.on_persistent_mode)
116 | if not self.app.gio_settings.get_value("close-button"):
117 | main_window.headerbar.set_show_close_button(False)
118 | main_window.word_label.props.margin_left = 10
119 |
120 | main_window.headerbar.hide()
121 | main_window.headerbar.show_all()
122 |
123 | if name == "sticky-mode":
124 | if switch.get_active():
125 | main_window.stick()
126 | else:
127 | main_window.unstick()
128 |
129 | if name == "close-button":
130 | if switch.get_active():
131 | main_window.word_label.props.margin_left = 0
132 | main_window.headerbar.set_show_close_button(True)
133 | else:
134 | main_window.word_label.props.margin_left = 10
135 | main_window.headerbar.set_show_close_button(False)
136 | main_window.headerbar.hide()
137 | main_window.headerbar.show_all()
138 |
139 |
140 |
141 |
142 | def on_checkbutton_activated(self, checkbutton, gparam, widget):
143 | name = checkbutton.get_name()
144 | theme_switch = widget
145 | if name == "theme-optin":
146 | if self.app.gio_settings.get_value("theme-optin"):
147 | prefers_color_scheme = self.app.granite_settings.get_prefers_color_scheme()
148 | sensitive = False
149 | else:
150 | prefers_color_scheme = Granite.SettingsColorScheme.NO_PREFERENCE
151 | theme_switch.switch.props.active = self.app.gio_settings.get_value("prefer-dark-style")
152 | sensitive = True
153 |
154 | self.app.gtk_settings.set_property("gtk-application-prefer-dark-theme", prefers_color_scheme)
155 | self.app.granite_settings.connect("notify::prefers-color-scheme", self.app.on_prefers_color_scheme)
156 |
157 | if "DARK" in prefers_color_scheme.value_name:
158 | active = True
159 | else:
160 | active = False
161 |
162 | theme_switch.switch.props.active = active
163 | theme_switch.props.sensitive = sensitive
164 |
165 | def on_appearance_style_change(self, granite_settings, gparam, widget):
166 | theme_switch = widget
167 | if theme_switch.switch.props.active:
168 | theme_switch.switch.props.active = False
169 | else:
170 | theme_switch.switch.props.active = True
171 |
172 |
173 | class SettingsGroup(Gtk.Grid):
174 | def __init__(self, group_label, subsettings_list, *args, **kwargs):
175 | super().__init__(*args, **kwargs)
176 |
177 | grid = Gtk.Grid()
178 | grid.props.margin = 8
179 | grid.props.hexpand = True
180 | grid.props.row_spacing = 8
181 | grid.props.column_spacing = 10
182 |
183 | i = 0
184 | for subsetting in subsettings_list:
185 | grid.attach(subsetting, 0, i, 1, 1)
186 | i += 1
187 |
188 | frame = Gtk.Frame()
189 | frame.props.name = "settings-group-frame"
190 | frame.props.hexpand = True
191 | frame.add(grid)
192 |
193 | label = Gtk.Label(group_label)
194 | label.props.name = "settings-group-label"
195 | label.props.halign = Gtk.Align.START
196 | label.props.margin_left = 4
197 |
198 | self.props.name = group_label
199 | self.props.halign = Gtk.Align.FILL
200 | self.props.hexpand = True
201 | self.props.row_spacing = 4
202 | self.props.can_focus = False
203 | self.attach(label, 0, 0, 1, 1)
204 | self.attach(frame, 0, 1, 1, 1)
205 |
206 |
207 | class SubSettings(Gtk.Grid):
208 | def __init__(self, type=None, name=None, label=None, sublabel=None, separator=True, params=None, utils=None, *args, **kwargs):
209 | super().__init__(*args, **kwargs)
210 |
211 | self.type = type
212 |
213 | # box---
214 | box = Gtk.VBox()
215 | box.props.spacing = 2
216 | box.props.hexpand = True
217 |
218 | # label---
219 | if label is not None:
220 | self.label_text = Gtk.Label(label)
221 | self.label_text.props.halign = Gtk.Align.START
222 | box.add(self.label_text)
223 |
224 | # sublabel---
225 | if sublabel is not None:
226 | self.sublabel_text = Gtk.Label(sublabel)
227 | self.sublabel_text.props.halign = Gtk.Align.START
228 | self.sublabel_text.props.wrap_mode = Pango.WrapMode.WORD
229 | self.sublabel_text.props.max_width_chars = 30
230 | self.sublabel_text.props.justify = Gtk.Justification.LEFT
231 | #self.sublabel_text.props.wrap = True
232 | self.sublabel_text.get_style_context().add_class("settings-sub-label")
233 | box.add(self.sublabel_text)
234 |
235 | if type == "switch":
236 | self.switch = Gtk.Switch()
237 | self.switch.props.name = name
238 | self.switch.props.halign = Gtk.Align.END
239 | self.switch.props.valign = Gtk.Align.CENTER
240 | self.switch.props.hexpand = False
241 | self.attach(self.switch, 1, 0, 1, 2)
242 |
243 | if type == "spinbutton":
244 | self.spinbutton = Gtk.SpinButton().new_with_range(min=params[0], max=params[1], step=params[2])
245 | self.spinbutton.props.name = name
246 | self.attach(self.spinbutton, 1, 0, 1, 2)
247 |
248 | if type == "button":
249 | if len(params) == 1:
250 | self.button = Gtk.Button(label=params[0])
251 | else:
252 | self.button = Gtk.Button(label=params[0], image=params[1])
253 | self.button.props.name = name
254 | self.button.props.hexpand = False
255 | self.button.props.always_show_image = True
256 | self.button.set_size_request(90, -1)
257 | if len(params) >1:
258 | label = [child for child in self.button.get_children()[0].get_child() if isinstance(child, Gtk.Label)][0]
259 | label.props.valign = Gtk.Align.CENTER
260 | self.attach(self.button, 1, 0, 1, 2)
261 |
262 | if type == "checkbutton":
263 | self.checkbutton = Gtk.CheckButton().new_with_label(params[0])
264 | self.checkbutton.props.name = name
265 | self.attach(self.checkbutton, 0, 0, 1, 2)
266 |
267 | # separator ---
268 | if separator:
269 | row_separator = Gtk.Separator()
270 | row_separator.props.hexpand = True
271 | row_separator.props.valign = Gtk.Align.CENTER
272 | if type == None:
273 | self.attach(row_separator, 0, 0, 1, 1)
274 | else:
275 | self.attach(row_separator, 0, 2, 2, 1)
276 |
277 | # SubSettings construct---
278 | self.props.name = name
279 | self.props.hexpand = True
280 | if type == None:
281 | self.attach(box, 0, 0, 1, 1)
282 | else:
283 | self.props.row_spacing = 8
284 | self.props.column_spacing = 10
285 | self.attach(box, 0, 0, 1, 2)
--------------------------------------------------------------------------------
/src/updater_view.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import os
5 | import gi
6 | from gi.repository import GLib
7 | gi.require_version('Gtk', '3.0')
8 | from gi.repository import Gtk, Pango, Gio
9 |
10 |
11 | #------------------CLASS-SEPARATOR------------------#
12 |
13 |
14 | class UpdaterView(Gtk.Grid):
15 | def __init__(self, *args, **kwargs):
16 | super().__init__(*args, **kwargs)
17 |
18 | #-- quickword logo --------#
19 | download_icon = Gtk.Image().new_from_icon_name("emblem-downloads", Gtk.IconSize.DIALOG)
20 | download_icon.props.halign = Gtk.Align.END
21 | download_icon.props.valign = Gtk.Align.START
22 | download_icon.props.expand = False
23 | download_icon.props.name = "download-icon"
24 | download_icon.get_style_context().add_class("quickword-icon-right")
25 |
26 | left_icon = Gtk.Image().new_from_icon_name("com.github.hezral.quickword-left", Gtk.IconSize.DIALOG)
27 | right_icon = Gtk.Image().new_from_icon_name("com.github.hezral.quickword-right", Gtk.IconSize.DIALOG)
28 |
29 | left_icon.set_pixel_size(96)
30 | right_icon.set_pixel_size(96)
31 |
32 | left_icon.props.expand = False
33 | right_icon.props.expand = False
34 |
35 | icon_overlay = Gtk.Overlay()
36 | icon_overlay.add(left_icon)
37 | icon_overlay.add_overlay(right_icon)
38 | icon_overlay.add_overlay(download_icon)
39 | icon_overlay.props.halign = Gtk.Align.CENTER
40 | icon_overlay.props.can_focus = True
41 | icon_overlay.props.focus_on_click = True
42 | icon_overlay.grab_focus()
43 |
44 | #-- message header --------#
45 |
46 | message = Gtk.Label()
47 | message.props.name = "message"
48 | message.props.margin_bottom = 5
49 | message.props.hexpand = True
50 | message.props.halign = Gtk.Align.CENTER
51 | message.props.valign = Gtk.Align.CENTER
52 | message.props.max_width_chars = 30
53 | message.props.wrap = True
54 | message.props.wrap_mode = Pango.WrapMode.WORD
55 | message.props.justify = Gtk.Justification.CENTER
56 | message.get_style_context().add_class("h2")
57 |
58 | sub_message = Gtk.Label()
59 | sub_message.props.name = "sub-message"
60 | sub_message.props.margin_bottom = 10
61 | sub_message.props.hexpand = True
62 | sub_message.props.halign = Gtk.Align.CENTER
63 | sub_message.props.valign = Gtk.Align.CENTER
64 | sub_message.props.max_width_chars = 50
65 | sub_message.props.wrap = True
66 | sub_message.props.wrap_mode = Pango.WrapMode.WORD
67 | sub_message.props.justify = Gtk.Justification.CENTER
68 |
69 | #-- proceed button --------#
70 | self.proceed_btn = Gtk.Button(label="Proceed")
71 | self.proceed_btn.props.name = "proceed-btn"
72 | self.proceed_btn.get_style_context().add_class("h3")
73 | self.proceed_btn.set_size_request(-1, 32)
74 | self.proceed_btn.connect("clicked", self.on_proceed_update)
75 |
76 | self.proceed_btn_revealer = Gtk.Revealer()
77 | self.proceed_btn_revealer.add(self.proceed_btn)
78 | self.proceed_btn_revealer.set_reveal_child(True)
79 |
80 | #-- start button --------#
81 | self.start_btn = Gtk.Button(label="Start Using Quickword")
82 | self.start_btn.props.name = "start-btn"
83 | self.start_btn.get_style_context().add_class("h3")
84 | self.start_btn.set_size_request(-1, 32)
85 | self.start_btn.connect("clicked", self.on_start)
86 |
87 | self.start_btn_revealer = Gtk.Revealer()
88 | self.start_btn_revealer.add(self.start_btn)
89 |
90 | #-- UpdaterView construct--------#
91 | self.props.name = "updater-view"
92 | self.get_style_context().add_class(self.props.name)
93 | self.set_size_request(350, -1)
94 | self.props.expand = True
95 | self.props.row_spacing = 12
96 | self.props.column_spacing = 6
97 | self.props.margin = 20
98 | self.props.valign = Gtk.Align.END
99 | self.attach(icon_overlay, 0, 1, 1, 1)
100 | self.attach(message, 0, 2, 1, 1)
101 | self.attach(sub_message, 0, 3, 1, 1)
102 | self.attach(self.proceed_btn_revealer, 0, 4, 1, 1)
103 | self.attach(self.start_btn_revealer, 0, 5, 1, 1)
104 | self.connect_after("realize", self.generate_message_str)
105 |
106 | def generate_message_str(self, view):
107 |
108 | stack = self.get_parent()
109 | window = stack.get_parent()
110 | app = window.props.application
111 |
112 | if app.first_run:
113 | message_str ="Hello!"
114 | sub_message_str = "On first run, dictionary download is required"
115 | sub_message_str = sub_message_str + "\n" + "Ensure internet connectivity before proceeding"
116 | else:
117 | message_str = "Check for Updates"
118 | sub_message_str = "Check for any updates to dictionary data"
119 | message = [child for child in self.get_children() if child.props.name == "message"][0]
120 | sub_message = [child for child in self.get_children() if child.props.name == "sub-message"][0]
121 |
122 | message.props.label = message_str
123 | sub_message.props.label = sub_message_str
124 |
125 | def on_start(self, button):
126 | stack = self.get_parent()
127 | window = stack.get_parent()
128 | app = window.props.application
129 | app.on_new_word_selected()
130 |
131 | def on_proceed_update(self, button):
132 | stack = self.get_parent()
133 | window = stack.get_parent()
134 | app = window.props.application
135 | icon_overlay = [child for child in self.get_children() if isinstance(child, Gtk.Overlay)][0]
136 | download_icon = [child for child in icon_overlay.get_children() if child.props.name == "download-icon"][0]
137 |
138 | download_icon.get_style_context().remove_class("quickword-icon-right")
139 | download_icon.get_style_context().add_class("download-icon-start")
140 |
141 | if app.first_run:
142 | app._data_manager.run_func(runname="download", callback=self.on_update_progress)
143 | gio_settings = Gio.Settings(schema_id="com.github.hezral.quickword")
144 | gio_settings.set_boolean("first-run", False)
145 | app.first_run = False
146 | else:
147 | run = app.generate_data_manager()
148 | while run is False:
149 | GLib.idle_add(self.on_update_progress, "One moment..", "Waiting for data manager")
150 | app._data_manager.run_func(runname="update", callback=self.on_update_progress)
151 |
152 | def on_update_progress(self, message_str, sub_message_str):
153 | message = [child for child in self.get_children() if child.props.name == "message"][0]
154 | sub_message = [child for child in self.get_children() if child.props.name == "sub-message"][0]
155 |
156 | if message_str == "Completed" or message_str == "Downloaded" or message_str == "No Updates":
157 | # start_btn = [child for child in self.get_children() if child.props.name == "start-btn"][0]
158 | # self.remove_row(4)
159 | # self.attach(start_btn, 0, 4, 1, 1)
160 | self.proceed_btn_revealer.set_reveal_child(False)
161 | self.start_btn_revealer.set_reveal_child(True)
162 |
163 | else:
164 | # proceed_btn = [child for child in self.get_children() if child.props.name == "proceed-btn"][0]
165 | self.proceed_btn.props.label = "Please wait.."
166 |
167 | message.props.label = message_str
168 | sub_message.props.label = sub_message_str
169 |
170 |
171 |
--------------------------------------------------------------------------------
/src/utils.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | class HelperUtils:
5 |
6 | @staticmethod
7 | def run_async(func):
8 | '''
9 | https://github.com/learningequality/ka-lite-gtk/blob/341813092ec7a6665cfbfb890aa293602fb0e92f/kalite_gtk/mainwindow.py
10 | http://code.activestate.com/recipes/576683-simple-threading-decorator/
11 | run_async(func):
12 | function decorator, intended to make "func" run in a separate thread (asynchronously).
13 | Returns the created Thread object
14 | Example:
15 | @run_async
16 | def task1():
17 | do_something
18 | @run_async
19 | def task2():
20 | do_something_too
21 | '''
22 | from threading import Thread
23 | from functools import wraps
24 |
25 | @wraps(func)
26 | def async_func(*args, **kwargs):
27 | func_hl = Thread(target=func, args=args, kwargs=kwargs)
28 | func_hl.start()
29 | # Never return anything, idle_add will think it should re-run the
30 | # function because it's a non-False value.
31 | return None
32 |
33 | return async_func
34 |
35 | @staticmethod
36 | def get_active_window_wm_class():
37 | ''' Function to get active window '''
38 | import Xlib
39 | import Xlib.display
40 |
41 | display = Xlib.display.Display()
42 | root = display.screen().root
43 |
44 | NET_ACTIVE_WINDOW = display.intern_atom('_NET_ACTIVE_WINDOW')
45 | WM_CLASS = display.intern_atom('WM_CLASS')
46 |
47 | root.change_attributes(event_mask=Xlib.X.FocusChangeMask)
48 | try:
49 | window_id = root.get_full_property(NET_ACTIVE_WINDOW, Xlib.X.AnyPropertyType).value[0]
50 | window = display.create_resource_object('window', window_id)
51 | try:
52 | return window.get_full_property(WM_CLASS, 0).value.replace(b'\x00',b' ').decode("utf-8").lower()
53 | except:
54 | return None
55 | except Xlib.error.XError: #simplify dealing with BadWindow
56 | return None
--------------------------------------------------------------------------------
/src/word_lookup.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import os
5 | import gi
6 | from gi.repository import GLib
7 |
8 | # nltk imports
9 | from nltk import data
10 | from nltk.corpus import wordnet as wn
11 |
12 | # for ipa output of words by espeak
13 | import subprocess
14 | from shutil import which, Error
15 |
16 | class WordLookup():
17 | def __init__(self, application_id="com.github.hezral.quickword", *args, **kwargs):
18 |
19 | # setup nltk data path
20 | nltk_data_path = os.path.join(GLib.get_user_data_dir(), application_id, 'nltk_data')
21 | data.path = [nltk_data_path]
22 |
23 | #check if espeak is installed
24 | try:
25 | self.espeak = which("espeak")
26 | except Error as error:
27 | print("espeak not installed")
28 |
29 | def get_synsets(self, word):
30 |
31 | # create list to store data to return
32 | # structure = word, pronounciation, synsets
33 | data_tuple = []
34 |
35 | # lowercase the word
36 | word = word.lower()
37 |
38 | # remove spaces in front and back
39 | word = word.strip()
40 |
41 | # remove special characters in front of word
42 | FirstContainsSpecialChars = any(not c.isalnum() for c in word[0])
43 | LastContainsSpecialChars = any(not c.isalnum() for c in word[len(word)-1])
44 |
45 | if FirstContainsSpecialChars:
46 | word = word[1:]
47 | if LastContainsSpecialChars:
48 | word = word[0:-1]
49 |
50 | # get synsets for word
51 | # wordnet lookup can only contain letters, numbers, spaces, hyphens, periods, slashes, and/or apostrophes.
52 | # check if safe chars and try to get synsets first
53 | containsSafeChar = set(" -./'_")
54 | if any((c in containsSafeChar) for c in word):
55 |
56 | synsets = wn.synsets(word)
57 | # if none returns
58 | if len(synsets) == 0:
59 | word = word.translate({ord(c): " " for c in " -./'_"}).split(" ")[0] #replace safe chars with space and only get for first word
60 |
61 | synsets = wn.synsets(word)
62 | else:
63 | synsets = wn.synsets(word)
64 |
65 | # clean up word for display, remove any special characters
66 | containsSpecialChars = any(not c.isalnum() for c in word)
67 | if containsSpecialChars:
68 | _word = word.translate({ord(c): " " for c in "!@#$%^&*()[]{};:,./<>?\|`~-=_+"})
69 | data_tuple.append(_word.title())
70 | else:
71 | data_tuple.append(word.capitalize())
72 |
73 | # get pronounciation
74 | try:
75 | run_espeak = subprocess.Popen([self.espeak, word, "-q", "--ipa"], stdout=subprocess.PIPE)
76 | stdout, stderr = run_espeak.communicate()
77 | pronounce = stdout.decode("utf-8").split("\n")[0].strip()
78 | except:
79 | pronounce = ["NA"]
80 | # add pronounciation to data list
81 |
82 | data_tuple.append(pronounce)
83 |
84 | # add to data list if there is any synset found
85 | if len(synsets) > 0:
86 | data_tuple.append(synsets)
87 |
88 | return data_tuple
89 |
90 | def get_totalwords(self):
91 | return len(wn._lemma_pos_offset_map)
92 |
93 |
94 | # wl = WordLookup()
95 |
96 | # print(type(wl))
97 |
98 | # wl.get_synsets("test")
99 |
100 | # the lines is only for debug
101 | # def lookup(clipboard=None, event=None, wd=None):
102 | # content, valid = clipboard_listener.copy_selected_text(clipboard)
103 | # if content and valid:
104 | # results = wd.get_synsets(content)
105 | # #print(results)
106 | # from clipboard import ClipboardListener
107 | # wd = WordLookup()
108 | # clipboard_listener = ClipboardListener()
109 | # clipboard_listener.copy_selected_text()
110 |
111 | # the lines is only for debug
112 | # clipboard_listener.clipboard.connect("owner-change", lookup, wd)
113 | # import gi, signal
114 | # gi.require_version('Gtk', '3.0')
115 | # from gi.repository import Gtk, GLib
116 | # GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
117 | # Gtk.main()
118 |
--------------------------------------------------------------------------------
/src/word_view.py:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-or-later
2 | # SPDX-FileCopyrightText: 2021 Adi Hezral
3 |
4 | import gi
5 | gi.require_version('Gtk', '3.0')
6 | from gi.repository import Gtk, Pango
7 |
8 | class WordView(Gtk.Grid):
9 | def __init__(self, *args, **kwargs):
10 | super().__init__(*args, **kwargs)
11 |
12 | self.clipboard_paste = None
13 | self.lookup_word = None
14 |
15 | self.stack = Gtk.Stack()
16 | self.stack.props.expand = True
17 | self.stack.props.transition_type = Gtk.StackTransitionType.CROSSFADE
18 |
19 | self.stack_switcher = Gtk.StackSwitcher()
20 | self.stack_switcher.props.homogeneous = True
21 | self.stack_switcher.props.stack = self.stack
22 | self.stack_switcher.get_style_context().add_class("subview-switcher")
23 |
24 | self.props.name = 'word-view'
25 | self.get_style_context().add_class(self.props.name)
26 | self.set_size_request(350, -1)
27 | self.props.expand = False
28 | self.attach(self.stack, 0, 0, 2, 1)
29 | self.attach(self.stack_switcher, 0, 1, 2, 1,)
30 |
31 | def on_wordlookup(self, button=None, data=None):
32 |
33 | self.lookup_word = data[0]
34 | synsets = data[2]
35 |
36 | stack = self.get_parent()
37 | window = stack.get_toplevel()
38 | window.pronounciation_label.props.label = "/ " + data[1] + " /"
39 | window.word_action_revealer.set_reveal_child(True)
40 |
41 | # delete all stack children if any
42 | if self.stack.get_children():
43 | for child in self.stack.get_children():
44 | self.stack.remove(child)
45 |
46 | # create list for each type of word
47 | noun_data = []
48 | adjective_data = []
49 | adverb_data = []
50 | ver_data = []
51 |
52 | # add data to each word list
53 | for synset in synsets:
54 | if synset.pos() == "n":
55 | noun_data.append(synset)
56 | if synset.pos() == "a" or synset.pos() == "s":
57 | adjective_data.append(synset)
58 | if synset.pos() == "r":
59 | adverb_data.append(synset)
60 | if synset.pos() == "v":
61 | ver_data.append(synset)
62 |
63 | # add word lists to dict
64 | word_list_dict = {}
65 | word_list_dict["noun"] = noun_data
66 | word_list_dict["adjective"] = adjective_data
67 | word_list_dict["adverb"] = adverb_data
68 | word_list_dict["verb"] = ver_data
69 |
70 | subviews = {}
71 | for type in word_list_dict:
72 | if len(word_list_dict[type]) > 0:
73 | subviews["{0}".format(type)] = WordSubView(name=type, word=self.lookup_word, contents=word_list_dict[type])
74 |
75 | # add views to stack
76 | for view in subviews:
77 | # subviews[view].show_all()
78 | self.stack.add_titled(subviews[view], view, view)
79 |
80 | # style left and right tabs for stack switcher
81 | stack_count = len(self.stack_switcher.get_children())
82 |
83 | left_tab = self.stack_switcher.get_children()[0]
84 | left_tab.get_style_context().add_class("word-types-left")
85 | right_tab = self.stack_switcher.get_children()[stack_count-1]
86 | right_tab.get_style_context().add_class("word-types-right")
87 |
88 | for child in self.stack_switcher.get_children():
89 | child.get_style_context().add_class("word-types")
90 | child.props.can_focus = False
91 |
92 | self.stack.show_all()
93 |
94 |
95 | #------------------CLASS-SEPARATOR------------------#
96 |
97 |
98 | class WordSubView(Gtk.Grid):
99 | def __init__(self, name, word, contents, *args, **kwargs):
100 | super().__init__(*args, **kwargs)
101 |
102 | self.props.name = name
103 | self.props.hexpand = True
104 | self.props.row_spacing = 15
105 | self.props.column_spacing = 0
106 |
107 | scrolled_view = Gtk.ScrolledWindow()
108 | scrolled_view.props.expand = True
109 | # scrolled_view.connect("edge-reached", self.on_edge_reached)
110 | # scrolled_view.connect("edge-overshot", self.on_edge_overshot)
111 |
112 | scrolled_view_grid = Gtk.Grid()
113 | scrolled_view_grid.props.row_spacing = 10
114 | scrolled_view_grid.props.expand = True
115 | scrolled_view_grid.props.margin = 10
116 | scrolled_view_grid.props.margin_left = 20
117 | scrolled_view_grid.props.margin_right = 20
118 |
119 | i = 1
120 | for content in contents:
121 | scrolled_view_grid.attach(WordItems(word, content), 0, i, 1, 1)
122 | i += 1
123 |
124 | scrolled_view.add(scrolled_view_grid)
125 | self.attach(scrolled_view, 0, 1, 1, 1)
126 |
127 | def on_edge_overshot(self, scrolledwindow, position):
128 | # print(position.value_name)
129 | # stack = self.get_parent()
130 | # word_view = stack.get_parent()
131 | # print(word_view)
132 | ...
133 |
134 | def on_edge_reached(self, scrolledwindow, position):
135 | # print(position.value_name)
136 | # stack = self.get_parent()
137 | # word_view = stack.get_parent()
138 | # print(word_view)
139 | ...
140 | # if position.value_name == "GTK_POS_BOTTOM":
141 | # self.count_label.props.label = str(self.more_count) + " more results up.."
142 | # elif position.value_name == "GTK_POS_TOP":
143 | # self.count_label.props.label = str(self.more_count) + " more results below.."
144 |
145 |
146 | #------------------CLASS-SEPARATOR------------------#
147 |
148 |
149 | class WordItems(Gtk.Grid):
150 | def __init__(self, word, contents, multi=False, *args, **kwargs):
151 | super().__init__(*args, **kwargs)
152 |
153 | # synset item
154 | synset = contents
155 |
156 | #-- word definition -------#
157 | definition_str = synset.definition()
158 |
159 | word_definition = Gtk.Label(definition_str)
160 | word_definition.props.wrap = True
161 | word_definition.props.hexpand = True
162 | word_definition.props.wrap_mode = Pango.WrapMode.WORD
163 | word_definition.props.justify = Gtk.Justification.FILL
164 | word_definition.props.halign = Gtk.Align.START
165 | word_definition.props.valign = Gtk.Align.CENTER
166 | word_definition.get_style_context().add_class("word-definition")
167 |
168 | #-- word examples -------#
169 | examples = synset.examples()
170 |
171 | if len(examples) > 0:
172 | examples_str = '"' + examples[0] + '"'
173 | else:
174 | examples_str = ""
175 |
176 | word_examples = Gtk.Label(examples_str)
177 | word_examples.props.wrap = True
178 | word_examples.props.hexpand = True
179 | word_examples.props.wrap_mode = Pango.WrapMode.WORD
180 | word_examples.props.justify = Gtk.Justification.FILL
181 | word_examples.props.halign = Gtk.Align.START
182 | word_examples.props.valign = Gtk.Align.START
183 | word_examples.get_style_context().add_class("word-examples")
184 |
185 | # box for definition and examples
186 | word_box = Gtk.VBox()
187 | word_box.props.hexpand = True
188 | word_box.set_size_request(-1, 24)
189 | word_box.add(word_definition)
190 | if not examples_str == "":
191 | word_definition.props.valign = Gtk.Align.START
192 | word_box.add(word_examples)
193 |
194 | #-- lemmas (similar words) -------#
195 | lemmas = synset.lemma_names()
196 | lemmas.sort(key=len)
197 |
198 | # remove lemma that is same with lookup word
199 | for lemma in lemmas:
200 | if lemma.lower() == word.lower():
201 | lemmas.remove(lemma)
202 |
203 | # grid to hold lemma till 5th item
204 | lemma_box = Gtk.Grid()
205 | lemma_box.props.column_spacing = 2
206 | lemma_box.props.row_spacing = 2
207 | lemma_box.props.hexpand = True
208 | lemma_box.props.halign = Gtk.Align.START
209 |
210 | # grid to hold lemma for 6th item onwards
211 | lemma_more_box = Gtk.Grid()
212 | lemma_more_box.props.margin_top = 10
213 | lemma_more_box.props.hexpand = False
214 | lemma_more_box.props.halign = Gtk.Align.START
215 | lemma_more_box.props.column_spacing = 2
216 | lemma_more_box.props.row_spacing = 4
217 | # encased into expander
218 | lemma_more_expander = Gtk.Expander()
219 | lemma_more_expander.props.margin_top = 3
220 | lemma_more_expander.add(lemma_more_box)
221 |
222 | # iterate through lemmas list
223 | i = j = k = 0
224 | charslen = 0
225 | charslenmore = 0
226 | if len(lemmas) > 1:
227 | for lemma in lemmas:
228 | # if text contains underscore and maybe other special characters
229 | # use lemma_label for cleaned string
230 | # use lemma_name for original string
231 | containsSpecialChars = any(not c.isalnum() for c in lemma)
232 | if containsSpecialChars:
233 | lemma_name = lemma.translate ({ord(c): " " for c in "!@#$%^&*()[]{};:,./<>?\|`~-=_+"})
234 | else:
235 | lemma_name = lemma
236 |
237 | charslen = len(lemma) + charslen
238 | if i < 6:
239 | if charslen <= 70:
240 | lemma_box.attach(self.generate_lemma_buttons(lemma_label=lemma_name, lemma_name=lemmas[i]), i, 1, 1, 1)
241 | else:
242 | charslenmore = len(lemma) + charslenmore
243 | if charslenmore <= 70:
244 | lemma_more_box.attach(self.generate_lemma_buttons(lemma_label=lemma_name, lemma_name=lemmas[i]), j, 1, 1, 1)
245 | j += 1
246 | else:
247 | lemma_more_box.attach(self.generate_lemma_buttons(lemma_label=lemma_name, lemma_name=lemmas[i]), k, 2, 1, 1) #need to recalculate i from zero and offset
248 | k += 1
249 | i += 1
250 |
251 | # add margin if lemma_more_box is present
252 | if len(lemma_more_box.get_children()) >= 1:
253 | lemma_box.props.margin_left = 16
254 |
255 | #-- copy action -------#
256 | copy_img = Gtk.Image().new_from_icon_name("edit-copy-symbolic", Gtk.IconSize.SMALL_TOOLBAR)
257 | copy_img.props.no_show_all = True
258 | copy_img.props.hexpand = True
259 | copy_img.props.halign = Gtk.Align.END
260 | copy_img.props.valign = Gtk.Align.END
261 | copy_img.get_style_context().add_class("transition-on")
262 | copy_img.get_style_context().add_class("copy-img")
263 |
264 | # copied notification
265 | copied_img = Gtk.Image().new_from_icon_name("emblem-default", Gtk.IconSize.SMALL_TOOLBAR)
266 | copied_img.props.no_show_all = True
267 | copied_label = Gtk.Label("Copied to clipboard")
268 | copied_label.props.no_show_all = True
269 |
270 | copied_grid = Gtk.Grid()
271 | copied_grid.props.column_spacing = 4
272 | copied_grid.props.halign = Gtk.Align.END
273 | copied_grid.props.valign = Gtk.Align.END
274 | copied_grid.props.hexpand = True
275 | copied_grid.get_style_context().add_class("transition-on")
276 | copied_grid.get_style_context().add_class("copied-content")
277 | copied_grid.attach(copied_img, 0, 1, 1, 1)
278 | copied_grid.attach(copied_label, 1, 1, 1, 1)
279 |
280 | # workaround to avoid weird issue with enter and notify events after adding to the eventbox
281 | # put all in a grid, then put in eventbox. overlay didn't work
282 | content_grid = Gtk.Grid()
283 | content_grid.props.row_spacing = 0
284 | content_grid.props.hexpand = True
285 | content_grid.attach(copy_img, 0, 1, 1, 2)
286 | content_grid.attach(copied_grid, 0, 1, 1, 2)
287 | content_grid.attach(word_box, 0, 1, 1, 1)
288 |
289 | #-- eventbox -------#
290 | content_eventbox = Gtk.EventBox()
291 | content_eventbox.add(content_grid)
292 |
293 | # list items to pass to eventbox events
294 | eventbox_params = (copy_img, copied_label, copied_img, copied_grid, word, word_definition, word_examples, word_box, lemma_box)
295 |
296 | content_eventbox.connect("enter-notify-event", self.on_enter_content_box, eventbox_params)
297 | content_eventbox.connect("leave-notify-event", self.on_leave_content_box, eventbox_params)
298 | content_eventbox.connect("button-press-event", self.on_copy_content_clicked, eventbox_params)
299 |
300 | #-- WordItems construct--------#
301 | # self.props.name = name
302 | self.props.hexpand = True
303 | self.props.row_spacing = 2
304 | self.props.column_spacing = 0
305 | self.attach(content_eventbox, 0, 1, 5, 1)
306 |
307 | if len(lemma_more_box) >= 1:
308 | self.attach(lemma_more_expander, 0, 2, 5, 1)
309 | self.attach(lemma_box, 1, 2, 1, 1)
310 | else:
311 | self.attach(lemma_box, 0, 2, 1, 1)
312 |
313 | def generate_lemma_buttons(self, lemma_label, lemma_name):
314 | button = Gtk.Button(label=lemma_label)
315 | button.props.name = lemma_name
316 | button.props.expand = False
317 | button.get_style_context().add_class("word-lemmas")
318 | button.connect("clicked", self.on_lemma_clicked)
319 | return button
320 |
321 | def on_lemma_clicked(self, button):
322 | wordview = self.get_wordview()
323 | stack = wordview.get_parent()
324 | window = stack.get_parent()
325 | app = window.props.application
326 | app.on_new_word_lookup(button.props.name)
327 |
328 | def generate_separator(self):
329 | separator = Gtk.Separator()
330 | separator.props.hexpand = True
331 | separator.props.valign = Gtk.Align.CENTER
332 | return separator
333 |
334 | def on_enter_content_box(self, eventbox, event, widget_list):
335 | copy_img = widget_list[0]
336 | copied_grid = widget_list[3]
337 | word_box = widget_list[7]
338 |
339 | # show widget and set flags to trigger transition effect, see application.css
340 | copy_img.show()
341 | copy_img.set_state_flags(Gtk.StateFlags.PRELIGHT, True)
342 |
343 | # set flags to ready state for transition effect, see application.css
344 | copied_grid.set_state_flags(Gtk.StateFlags.DIR_LTR, True)
345 |
346 | # add styling for hover effect
347 | word_box.get_children()[0].get_style_context().add_class("word-hover")
348 | try:
349 | word_box.get_children()[1].get_style_context().remove_class("word-examples")
350 | word_box.get_children()[1].get_style_context().add_class("word-examples-hover")
351 | except:
352 | pass
353 |
354 | def on_leave_content_box(self, eventbox, event, widget_list):
355 | copy_img = widget_list[0]
356 | copied_grid = widget_list[3]
357 | word_box = widget_list[7]
358 |
359 | # reset state flagss to ready state for transition effect, see application.css
360 | copy_img.set_state_flags(Gtk.StateFlags.DIR_LTR, True)
361 |
362 | # reset state flags to ready state for transition effect, see application.css
363 | # grid can stay as show state since only toggle widget hide/shows
364 | copied_grid.set_state_flags(Gtk.StateFlags.DIR_LTR, True)
365 |
366 | # remove styling for hover effect
367 | word_box.get_children()[0].get_style_context().remove_class("word-hover")
368 | try:
369 | word_box.get_children()[1].get_style_context().remove_class("word-examples-hover")
370 | word_box.get_children()[1].get_style_context().add_class("word-examples")
371 | except:
372 | pass
373 |
374 | def on_copy_content_clicked(self, eventbox, event, widget_list):
375 | copy_img = widget_list[0]
376 | copied_label = widget_list[1]
377 | copied_img = widget_list[2]
378 | copied_grid = widget_list[3]
379 | word = widget_list[4]
380 | word_definition = widget_list[5]
381 | word_example = widget_list[6]
382 | word_lemmas = widget_list[8]
383 | word_lemmas_list = []
384 | if len(word_lemmas.get_children()) > 0:
385 | for child in word_lemmas.get_children():
386 | word_lemmas_list.append(child.props.label)
387 |
388 | # reset state flagss to ready state for transition effect, see application.css
389 | copy_img.set_state_flags(Gtk.StateFlags.DIR_LTR, True)
390 |
391 | # show widgets
392 | copied_label.show()
393 | copied_img.show()
394 |
395 | # set flags to trigger transition effect, see application.css
396 | copied_grid.set_state_flags(Gtk.StateFlags.PRELIGHT, True)
397 |
398 | # callback to copy content to clipboard
399 | clipboard_paste = self.get_wordview().clipboard_paste
400 | to_copy = "Word: " + word + "\n"
401 | to_copy = to_copy + "Pronounciation: " + self.get_wordview().get_toplevel().pronounciation_label.props.label + "\n"
402 | to_copy = to_copy + "Definition: " + word_definition.props.label + "\n"
403 | to_copy = to_copy + "Example: " + word_example.props.label + "\n"
404 | to_copy = to_copy + "Synonyms/Related: " + ", ".join(word_lemmas_list)
405 | clipboard_paste.copy_to_clipboard(to_copy)
406 |
407 | def get_wordview(self):
408 | worditem = self
409 | # if not in scrolled view
410 | if isinstance(worditem.get_parent(), WordSubView):
411 | wordsubview = worditem.get_parent() #WordSubView
412 | stack = wordsubview.get_parent() #GtkStack
413 | wordview = stack.get_parent() #WordView
414 | # in scrolled view
415 | else:
416 | worditemgrid = worditem.get_parent() #GtkGrid
417 | viewport = worditemgrid.get_parent() #GtkViewport
418 | scrolledwindow = viewport.get_parent() #GtkScrolledWindow
419 | wordsubview = scrolledwindow.get_parent() #WordSubView
420 | stack = wordsubview.get_parent() #GtkStack
421 | wordview = stack.get_parent() #WordView
422 | return wordview
423 |
--------------------------------------------------------------------------------