├── .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 | Magnus 3 |
4 | Magnus 5 |

6 | 7 |

A very simple screen magnifier.

8 | 9 | ![Magnus Screenshot](data/screenshot.png?raw=true) 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 | --------------------------------------------------------------------------------