├── .gitignore
├── .github
└── FUNDING.yml
├── data
├── logo.png
├── screenshot.png
├── magnus-autostart.desktop
├── magnus.desktop
└── magnus.1
├── .travis.yml
├── LICENSE
├── README.md
├── setup.py
├── snap
└── snapcraft.yaml
└── magnus
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.egg-info/
3 | build/
4 | dist/
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: https://paypal.me/stuartlangridge
2 |
--------------------------------------------------------------------------------
/data/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuartlangridge/magnus/HEAD/data/logo.png
--------------------------------------------------------------------------------
/data/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stuartlangridge/magnus/HEAD/data/screenshot.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | arch:
2 | - amd64
3 | - ppc64le
4 | language: python
5 | python:
6 | - "3.6"
7 | install: "pip install flake8"
8 | script:
9 | - flake8 magnus
10 |
--------------------------------------------------------------------------------
/data/magnus-autostart.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Type=Application
3 | Name=Magnus screen magnifier
4 | Exec=magnus --started-by-keypress
5 | NoDisplay=true
6 | AutostartCondition=GSettings org.gnome.desktop.a11y.applications screen-magnifier-enabled
7 | X-MATE-AutoRestart=true
8 | X-GNOME-AutoRestart=true
9 |
--------------------------------------------------------------------------------
/data/magnus.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Magnus
3 | GenericName=Magnus screen magnifier
4 | Comment=A very simple screen magnifier
5 | Exec=magnus
6 | Terminal=false
7 | Type=Application
8 | Categories=Utility;Accessibility;
9 | Keywords=magnifier;accessibility;utility;
10 | Icon=zoom-best-fit
11 |
12 |
--------------------------------------------------------------------------------
/data/magnus.1:
--------------------------------------------------------------------------------
1 | .TH magnus 1 "" ""
2 | .SH NAME
3 | magnus \- A very simple screen magnifier
4 | .SH SYNOPSIS
5 | .B magnus [--about] [--refresh-interval=MILLISECONDS]
6 | .SH DESCRIPTION
7 | A very simple screen magnifier for visually impaired users.
8 | Allows setting the zoom level to anything between 2x and 5x.
9 | .SH OPTIONS
10 | .TP
11 | .BR \-\-refresh-interval =\fIMILLISECONDS\fR
12 | How often to update Magnus's magnifier view. Defaults to 120ms.
13 |
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Stuart Langridge
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Magnus
5 |
6 |
7 | A very simple screen magnifier.
8 |
9 | 
10 |
11 | Made with 💝 for 
12 |
13 | ## Building, Testing, and Installation
14 |
15 | ### Ubuntu
16 |
17 | A [PPA for Magnus](https://launchpad.net/~flexiondotorg/+archive/ubuntu/magnus) is published by [Martin Wimpress](https://github.com/flexiondotorg).
18 |
19 | ```bash
20 | sudo add-apt-repository ppa:flexiondotorg/magnus
21 | sudo apt update
22 | sudo apt install magnus
23 | ```
24 |
25 | There's also an Ansible deployment by Taha Ahmed at [codeberg](https://codeberg.org/ansible/magnus).
26 |
27 | ### Source
28 |
29 | You'll need the following dependencies:
30 |
31 | * `gir1.2-gdkpixbuf-2.0`
32 | * `gir1.2-glib-2.0`
33 | * `gir1.2-gtk-3.0`
34 | * `gir1.2-keybinder-3.0`
35 | * `python3`
36 | * `python3-gi`
37 | * `python3-setproctitle`
38 |
39 | Run `setup.py` to build and install Magnus:
40 |
41 | ```bash
42 | sudo python3 setup.py install
43 | ```
44 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | import sys
6 |
7 | from glob import glob
8 | from setuptools import setup
9 |
10 | import DistUtilsExtra.command.build_extra
11 | import DistUtilsExtra.command.build_i18n
12 | import DistUtilsExtra.command.clean_i18n
13 |
14 | # to update i18n .mo files (and merge .pot file into .po files) run on Linux:
15 | # tx pull -a --minimum-perc=5
16 | # python3 setup.py build_i18n -m
17 | # tx push -s
18 |
19 | # silence pyflakes, __VERSION__ is properly assigned below...
20 | __VERSION__ = '0.0.0.0'
21 | with open('magnus') as f:
22 | for line in f:
23 | if (line.startswith('__VERSION__')):
24 | exec(line.strip())
25 | break
26 |
27 | PROGRAM_VERSION = __VERSION__
28 |
29 | def datafilelist(installbase, sourcebase):
30 | datafileList = []
31 | for root, subFolders, files in os.walk(sourcebase):
32 | fileList = []
33 | for f in files:
34 | fileList.append(os.path.join(root, f))
35 | datafileList.append((root.replace(sourcebase, installbase), fileList))
36 | return datafileList
37 |
38 | data_files = [
39 | ('{prefix}/share/man/man1'.format(prefix=sys.prefix), glob('data/*.1')),
40 | ('{prefix}/share/applications'.format(prefix=sys.prefix), ['data/magnus.desktop',]),
41 | ('/etc/xdg/autostart'.format(prefix=sys.prefix), ['data/magnus-autostart.desktop',]),
42 | ]
43 | #data_files.extend(datafilelist('{prefix}/share/locale'.format(prefix=sys.prefix), 'build/mo'))
44 |
45 | cmdclass ={
46 | "build" : DistUtilsExtra.command.build_extra.build_extra,
47 | "build_i18n" : DistUtilsExtra.command.build_i18n.build_i18n,
48 | "clean": DistUtilsExtra.command.clean_i18n.clean_i18n,
49 | }
50 |
51 | setup(
52 | name = "Magnus",
53 | version = PROGRAM_VERSION,
54 | description = "A very simple screen magnifier for Ubuntu",
55 | license = 'MIT',
56 | author = 'Stuart Langridge',
57 | url = 'https://github.com/stuartlangridge/magnus',
58 | package_dir = {'': '.'},
59 | data_files = data_files,
60 | install_requires = [ 'setuptools', ],
61 | scripts = ['magnus'],
62 | cmdclass = cmdclass,
63 | )
64 |
--------------------------------------------------------------------------------
/snap/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: magnus
2 | version: git
3 | version-script: |
4 | VER=$(grep __VERSION__ magnus | head -n 1 | cut -d'=' -f2 | sed 's/ //g' | sed "s/\"//g")
5 | REV=$(git rev-parse --short HEAD)
6 | echo $VER-$REV
7 | summary: Magnus screen magnifier
8 | description: |
9 | Magnus is a very simple desktop magnifier, showing the area around the mouse pointer in a separate window magnified two, three, four, or five times. Useful for users who need magnification, whether to help with eyesight or for accurate graphical design or detail work.
10 | icon: data/logo.png
11 |
12 | base: core18
13 | grade: stable
14 | confinement: strict
15 |
16 | plugs:
17 | gnome-3-28-1804:
18 | interface: content
19 | target: gnome-platform
20 | default-provider: gnome-3-28-1804:gnome-3-28-1804
21 | gtk-3-themes:
22 | interface: content
23 | target: $SNAP/share/themes
24 | default-provider: gtk-common-themes:gtk-3-themes
25 | icon-themes:
26 | interface: content
27 | target: $SNAP/share/icons
28 | default-provider: gtk-common-themes:icon-themes
29 | sound-themes:
30 | interface: content
31 | target: $SNAP/share/sounds
32 | default-provider: gtk-common-themes:sounds-themes
33 |
34 | slots:
35 | magnus-dbus:
36 | interface: dbus
37 | name: org.kryogenix.magnus
38 | bus: session
39 |
40 | apps:
41 | magnus:
42 | command: desktop-launch $SNAP/bin/magnus
43 | desktop: usr/share/applications/magnus.desktop
44 | plugs:
45 | - desktop
46 | - desktop-legacy
47 | - gsettings
48 | - home
49 | - wayland
50 | - x11
51 |
52 | parts:
53 | desktop-gtk3:
54 | build-packages:
55 | - libgtk-3-dev
56 | make-parameters:
57 | - FLAVOR=gtk3
58 | plugin: make
59 | source: https://github.com/ubuntu/snapcraft-desktop-helpers.git
60 | source-subdir: gtk
61 |
62 | # Python's DistUtilsExtra causes pip install failure
63 | # https://forum.snapcraft.io/t/use-of-pythons-distutilsextra-causes-pip-install-failure/802
64 | magnus:
65 | after:
66 | - desktop-gtk3
67 | source: .
68 | plugin: nil
69 | stage-packages:
70 | - gir1.2-gdkpixbuf-2.0
71 | - gir1.2-glib-2.0
72 | - gir1.2-gtk-3.0
73 | - gir1.2-keybinder-3.0
74 | - python3-gi
75 | - python3-setproctitle
76 | override-build: |
77 | snapcraftctl build
78 | install -D -m 755 -o root magnus $SNAPCRAFT_PART_INSTALL/bin/magnus
79 | install -D -m 644 -o root data/logo.png $SNAPCRAFT_PART_INSTALL/usr/share/pixmaps/magnus.png
80 | install -D -m 644 -o root data/magnus.desktop $SNAPCRAFT_PART_INSTALL/usr/share/applications/magnus.desktop
81 | sed -i 's|Icon=zoom-best-fit|Icon=/usr/share/pixmaps/magnus\.png|' ${SNAPCRAFT_PART_INSTALL}/usr/share/applications/magnus.desktop
82 | # Most of what Magnus requires is provided by the GNOME platform snap; so just prime what we need.
83 | prime:
84 | - bin
85 | - command-*.wrapper
86 | - flavor-select
87 | - lib
88 | - usr/bin/python*
89 | - usr/lib/girepository-1.0
90 | - usr/lib/python3*
91 | - usr/lib/*/gio
92 | - usr/lib/*/girepository-1.0
93 | - usr/lib/*/libkeybinder*
94 | - usr/share/applications/magnus.desktop
95 | - usr/share/pixmaps/magnus.png
96 |
--------------------------------------------------------------------------------
/magnus:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import json
4 | import os
5 | import codecs
6 | import setproctitle
7 | import sys
8 | from functools import lru_cache
9 | import gi
10 | gi.require_version('Gtk', '3.0')
11 | gi.require_version('Keybinder', '3.0')
12 | from gi.repository import \
13 | Gtk, Gdk, GLib, GdkPixbuf, Gio, Keybinder # noqa: E402
14 |
15 | __VERSION__ = "1.0.4"
16 |
17 |
18 | class Main(object):
19 | def __init__(self):
20 | self.zoomlevel = 2
21 | self.app = Gtk.Application.new(
22 | "org.kryogenix.magnus",
23 | Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
24 | self.app.connect("command-line", self.handle_commandline)
25 | self.app.connect("shutdown", self.handle_shutdown)
26 | self.resize_timeout = None
27 | self.window_metrics = None
28 | self.window_metrics_restored = False
29 | self.decorations_height = 0
30 | self.decorations_width = 0
31 | self.min_width = 300
32 | self.min_height = 300
33 | self.last_x = -1
34 | self.last_y = -1
35 | self.refresh_interval = 250
36 | self.started_by_keypress = False
37 | self.force_refresh = False
38 |
39 | def handle_shutdown(self, app):
40 | if self.started_by_keypress:
41 | settings = Gio.Settings.new("org.gnome.desktop.a11y.applications")
42 | val = settings.get_boolean("screen-magnifier-enabled")
43 | if val:
44 | settings.set_boolean("screen-magnifier-enabled", False)
45 |
46 | def handle_commandline(self, app, cmdline):
47 | args = cmdline.get_arguments()
48 | if hasattr(self, "w"):
49 | # already started
50 | if "--about" in args:
51 | self.show_about_dialog()
52 | return 0
53 |
54 | if "--help" in args:
55 | print("Options:")
56 | print(" --about")
57 | print(" Show about dialogue")
58 | print(" --refresh-interval=120")
59 | print(" Set refresh interval in milliseconds (lower is faster)")
60 | print(" --force-refresh")
61 | print(" Refresh continually (according to refresh interval)")
62 | print(" even if the mouse has not moved")
63 | return 0
64 |
65 | if "--force-refresh" in args:
66 | # If this argument is supplied, refresh the view even if the mouse
67 | # has not moved. Useful if the screen content is video.
68 | self.force_refresh = True
69 |
70 | # Override refresh rate on command line
71 | for arg in args:
72 | if arg.startswith("--refresh-interval="):
73 | parts = arg.split("=")
74 | if len(parts) == 2:
75 | try:
76 | rival = int(parts[1])
77 | print("Refresh interval set to {}ms".format(rival))
78 | self.refresh_interval = rival
79 | except ValueError:
80 | pass
81 | if arg == "--started-by-keypress":
82 | self.started_by_keypress = True
83 | # This is here so that the autostart desktop file can
84 | # specify it.
85 | # The idea is that Gnome-ish desktops have an explicit
86 | # keybinding to run the system magnifier; what this keybinding
87 | # actually does, via the {desktop}-settings-daemon, is toggle
88 | # the gsettings key
89 | # org.gnome.desktop.a11y.applications screen-magnifier-enabled
90 | # Magnus provides a desktop file to go in /etc/xdg/autostart
91 | # which contains an AutostartCondition of
92 | # GSettings org.gnome.desktop.a11y.applications /
93 | # screen-magnifier-enabled
94 | # and then the {desktop}-session daemon takes care of
95 | # starting the app when that key goes true, and closing the
96 | # app if that key goes false. However, the user may also
97 | # explicitly quit Magnus with the close icon or alt-f4
98 | # or similar. If they do so, then we explicitly set the key
99 | # back to false, so that the global keybinding to run the
100 | # magnifier stays in sync.
101 |
102 | # First time startup
103 | self.start_everything_first_time()
104 | if "--about" in args:
105 | self.show_about_dialog()
106 | return 0
107 |
108 | def start_everything_first_time(self, on_window_map=None):
109 | GLib.set_application_name("Magnus")
110 |
111 | # the window
112 | self.w = Gtk.ApplicationWindow.new(self.app)
113 | self.w.set_size_request(self.min_width, self.min_height)
114 | self.w.set_title("Magnus")
115 | self.w.connect("destroy", lambda a: self.app.quit())
116 | self.w.connect("configure-event", self.read_window_size)
117 | self.w.connect("configure-event", self.window_configure)
118 | self.w.connect("size-allocate", self.read_window_decorations_size)
119 | devman = self.w.get_screen().get_display().get_device_manager()
120 | self.pointer = devman.get_client_pointer()
121 |
122 | # the zoom chooser
123 | zoom = Gtk.ComboBoxText.new()
124 | self.zoom = zoom
125 | for i in range(2, 6):
126 | zoom.append(str(i), "{}×".format(i))
127 | zoom.set_active(0)
128 | zoom.connect("changed", self.set_zoom)
129 |
130 | # the box that contains everything
131 | self.img = Gtk.Image()
132 | scrolled_window = Gtk.ScrolledWindow()
133 | scrolled_window.add(self.img)
134 |
135 | # headerbar or no csd
136 | use_headerbar = True
137 | if "GTK_CSD" in os.environ:
138 | gtk_csd = os.environ.get("GTK_CSD")
139 | if gtk_csd == "0" or gtk_csd == "no" or gtk_csd == "":
140 | use_headerbar = False
141 | if use_headerbar:
142 | # the headerbar
143 | head = Gtk.HeaderBar()
144 | head.set_show_close_button(True)
145 | head.props.title = "Magnus"
146 | self.w.set_titlebar(head)
147 | head.pack_end(zoom)
148 | self.w.add(scrolled_window)
149 | else:
150 | # use regular assets
151 | scrolled_window.set_hexpand(True)
152 | scrolled_window.set_vexpand(True)
153 | grid = Gtk.Grid(column_homogeneous=False)
154 | grid.add(zoom)
155 | grid.attach(scrolled_window,0,1,4,4)
156 | self.w.add(grid)
157 |
158 | # bind the zoom keyboard shortcuts
159 | Keybinder.init()
160 | if Keybinder.supported():
161 | Keybinder.bind("plus", self.zoom_in, zoom)
162 | Keybinder.bind("equal", self.zoom_in, zoom)
163 | Keybinder.bind("minus", self.zoom_out, zoom)
164 |
165 | # and, go
166 | self.w.show_all()
167 |
168 | self.width = 0
169 | self.height = 0
170 | self.window_x = 0
171 | self.window_y = 0
172 | GLib.timeout_add(250, self.read_window_size)
173 |
174 | # and, poll
175 | GLib.timeout_add(self.refresh_interval, self.poll)
176 |
177 | GLib.idle_add(self.load_config)
178 |
179 | def zoom_out(self, keypress, zoom):
180 | current_index = zoom.get_active()
181 | if current_index == 0:
182 | return
183 | zoom.set_active(current_index - 1)
184 | self.set_zoom(zoom)
185 |
186 | def zoom_in(self, keypress, zoom):
187 | current_index = zoom.get_active()
188 | size = zoom.get_model().iter_n_children(None)
189 | if current_index == size - 1:
190 | return
191 | zoom.set_active(current_index + 1)
192 | self.set_zoom(zoom)
193 |
194 | def read_window_decorations_size(self, win, alloc):
195 | sz = self.w.get_size()
196 | self.decorations_width = alloc.width - sz.width
197 | self.decorations_height = alloc.height - sz.height
198 |
199 | def set_zoom(self, zoom):
200 | self.zoomlevel = int(zoom.get_active_text()[0])
201 | self.poll(force_refresh=True)
202 | self.serialise()
203 |
204 | def read_window_size(self, *args):
205 | loc = self.w.get_size()
206 | self.width = loc.width
207 | self.height = loc.height
208 |
209 | def show_about_dialog(self, *args):
210 | about_dialog = Gtk.AboutDialog()
211 | about_dialog.set_artists(["Stuart Langridge"])
212 | about_dialog.set_authors(["Stuart Langridge"])
213 | about_dialog.set_version(__VERSION__)
214 | about_dialog.set_license_type(Gtk.License.MIT_X11)
215 | about_dialog.set_website("https://www.kryogenix.org/code/magnus")
216 | about_dialog.run()
217 | if about_dialog:
218 | about_dialog.destroy()
219 |
220 | @lru_cache()
221 | def makesquares(self, overall_width, overall_height, square_size,
222 | value_on, value_off):
223 | on_sq = list(value_on) * square_size
224 | off_sq = list(value_off) * square_size
225 | on_row = []
226 | off_row = []
227 | while len(on_row) < overall_width * len(value_on):
228 | on_row += on_sq
229 | on_row += off_sq
230 | off_row += off_sq
231 | off_row += on_sq
232 | on_row = on_row[:overall_width * len(value_on)]
233 | off_row = off_row[:overall_width * len(value_on)]
234 |
235 | on_sq_row = on_row * square_size
236 | off_sq_row = off_row * square_size
237 |
238 | overall = []
239 | count = 0
240 | while len(overall) < overall_width * overall_height * len(value_on):
241 | overall += on_sq_row
242 | overall += off_sq_row
243 | count += 2
244 | overall = overall[:overall_width * overall_height * len(value_on)]
245 | return overall
246 |
247 | @lru_cache()
248 | def get_white_pixbuf(self, width, height):
249 | square_size = 16
250 | light = (153, 153, 153, 255)
251 | dark = (102, 102, 102, 255)
252 | whole = self.makesquares(width, height, square_size, light, dark)
253 | arr = GLib.Bytes.new(whole)
254 | return GdkPixbuf.Pixbuf.new_from_bytes(
255 | arr, GdkPixbuf.Colorspace.RGB, True, 8,
256 | width, height, width * len(light))
257 |
258 | def poll(self, force_refresh=False):
259 | display = Gdk.Display.get_default()
260 | (screen, x, y, modifier) = display.get_pointer()
261 | if x == self.last_x and y == self.last_y:
262 | # bail if nothing would be different
263 | if not force_refresh and not self.force_refresh:
264 | return True
265 | self.last_x = x
266 | self.last_y = y
267 | if (x > self.window_x and
268 | x <= (self.window_x + self.width + self.decorations_width) and
269 | y > self.window_y and
270 | y <= (self.window_y + self.height + self.decorations_height)):
271 | # pointer is over our window, so make it an empty pixbuf
272 | white = self.get_white_pixbuf(self.width, self.height)
273 | self.img.set_from_pixbuf(white)
274 | else:
275 | root = Gdk.get_default_root_window()
276 | scaled_width = self.width // self.zoomlevel
277 | scaled_height = self.height // self.zoomlevel
278 | scaled_xoff = scaled_width // 2
279 | scaled_yoff = scaled_height // 2
280 | screenshot = Gdk.pixbuf_get_from_window(
281 | root, x - scaled_xoff,
282 | y - scaled_yoff, scaled_width, scaled_height)
283 | scaled_pb = screenshot.scale_simple(
284 | self.width, self.height,
285 | GdkPixbuf.InterpType.NEAREST)
286 | self.img.set_from_pixbuf(scaled_pb)
287 | return True
288 |
289 | def window_configure(self, window, ev):
290 | if not self.window_metrics_restored:
291 | return False
292 | if self.resize_timeout:
293 | GLib.source_remove(self.resize_timeout)
294 | self.resize_timeout = GLib.timeout_add_seconds(
295 | 1, self.save_window_metrics_after_timeout,
296 | {"x": ev.x, "y": ev.y, "w": ev.width, "h": ev.height})
297 | self.window_x = ev.x
298 | self.window_y = ev.y
299 |
300 | def save_window_metrics_after_timeout(self, props):
301 | GLib.source_remove(self.resize_timeout)
302 | self.resize_timeout = None
303 | self.save_window_metrics(props)
304 |
305 | def save_window_metrics(self, props):
306 | scr = self.w.get_screen()
307 | sw = float(scr.get_width())
308 | sh = float(scr.get_height())
309 | # We save window dimensions as fractions of the screen dimensions,
310 | # to cope with screen resolution changes while we weren't running
311 | self.window_metrics = {
312 | "ww": props["w"] / sw,
313 | "wh": props["h"] / sh,
314 | "wx": props["x"] / sw,
315 | "wy": props["y"] / sh
316 | }
317 | self.serialise()
318 |
319 | def restore_window_metrics(self, metrics):
320 | scr = self.w.get_screen()
321 | sw = float(scr.get_width())
322 | sh = float(scr.get_height())
323 | self.w.set_size_request(self.min_width, self.min_height)
324 | self.w.resize(
325 | int(sw * metrics["ww"]), int(sh * metrics["wh"]))
326 | self.w.move(int(sw * metrics["wx"]), int(sh * metrics["wy"]))
327 |
328 | def get_cache_file(self):
329 | return os.path.join(GLib.get_user_cache_dir(), "magnus.json")
330 |
331 | def serialise(self, *args, **kwargs):
332 | # yeah, yeah, supposed to use Gio's async file stuff here. But it was
333 | # writing corrupted files, and I have no idea why; probably the Python
334 | # var containing the data was going out of scope or something. Anyway,
335 | # we're only storing a small JSON file, so life's too short to hammer
336 | # on this; we'll write with Python and take the hit.
337 | fp = codecs.open(self.get_cache_file(), encoding="utf8", mode="w")
338 | data = {"zoom": self.zoomlevel}
339 | if self.window_metrics:
340 | data["metrics"] = self.window_metrics
341 | json.dump(data, fp, indent=2)
342 | fp.close()
343 |
344 | def load_config(self):
345 | f = Gio.File.new_for_path(self.get_cache_file())
346 | f.load_contents_async(None, self.finish_loading_history)
347 |
348 | def finish_loading_history(self, f, res):
349 | try:
350 | success, contents, _ = f.load_contents_finish(res)
351 | except GLib.Error as e:
352 | print(("couldn't restore settings (error: %s)"
353 | ", so assuming they're blank") % (e,))
354 | contents = "{}"
355 |
356 | try:
357 | data = json.loads(contents)
358 | except Exception as e:
359 | print(("Warning: settings file seemed to be invalid json"
360 | " (error: %s), so assuming blank") % (e,))
361 | data = {}
362 | zl = data.get("zoom")
363 | if zl:
364 | idx = 0
365 | for row in self.zoom.get_model():
366 | text, lid = list(row)
367 | if lid == str(zl):
368 | self.zoom.set_active(idx)
369 | self.zoomlevel = zl
370 | idx += 1
371 | metrics = data.get("metrics")
372 | if metrics:
373 | self.restore_window_metrics(metrics)
374 | self.window_metrics_restored = True
375 |
376 |
377 | def main():
378 | setproctitle.setproctitle('magnus')
379 | m = Main()
380 | m.app.run(sys.argv)
381 |
382 |
383 | if __name__ == "__main__":
384 | main()
385 |
--------------------------------------------------------------------------------