Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,8 @@ Where that README names a Windows version, read the Linux one beside it:
| 3.0.0-alpha.8 | 1.0.0-alpha.8 | security fix: signed vault keys (Server 2.0.0-alpha.5) |
| 3.0.0 | 1.0.0 | SecretSpace 3, the first release: everything from the alpha rows (Server 2.0.0) |
| 3.1.0-alpha.1, alpha.2 | 1.1.0-alpha.2 | the new look: light and dark, the Manrope typeface, vaults down the left of the main window |
| — | 1.1.0-alpha.3 | Linux only: the desktop is asked for light or dark once, not every 30 ms |
| 3.1.0-alpha.3 | 1.1.0-alpha.4 | icons drawn by the app, vault tiles in the Vaults window, no outlined cards |

(0.2.0 added the `.deb` package, which has no Windows counterpart.)

Expand Down
3 changes: 1 addition & 2 deletions sourceFiles/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,7 @@ def _phrase_dialog(app, new, password):
"them on paper, in order."))
wrap = shell.body

grid = ctk.CTkFrame(wrap, fg_color=CARD, corner_radius=16,
border_width=1, border_color=BORDER)
grid = ctk.CTkFrame(wrap, fg_color=CARD, corner_radius=16)
grid.pack(fill="x", pady=(14, 0))
for c in range(3):
grid.grid_columnconfigure(c, weight=1, uniform="word")
Expand Down
55 changes: 44 additions & 11 deletions sourceFiles/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import changelog
import dialogs
import history_ui
from icons import icon
import lockout
import sync_client
import sync_ui
Expand Down Expand Up @@ -58,6 +59,7 @@
LOGIN_TILES_PER_ROW = 3

BACKDROP_REDRAW_MS = 60 # the login backdrop waits for a resize to settle
BACKDROP_MODE_MS = 1000 # how often the login screen looks for a mode change
BACKDROP_OVERSAMPLE = 3 # painted this many times larger, then scaled down

#: The vault filter's entry that shows every vault of the account.
Expand Down Expand Up @@ -278,6 +280,7 @@ def __init__(self, root):
self._idle_job = None
self._lockout_job = None
self._backdrop_job = None
self._mode_job = None
self._last_activity = time.monotonic()
self._autolock_seconds = appconfig.DEFAULT_AUTOLOCK_MINUTES * 60
self._syncing = False
Expand Down Expand Up @@ -497,6 +500,9 @@ def _clear(self):
if self._backdrop_job:
self.root.after_cancel(self._backdrop_job)
self._backdrop_job = None
if self._mode_job:
self.root.after_cancel(self._mode_job)
self._mode_job = None
for w in self.root.winfo_children():
w.destroy()
self._toast = None
Expand Down Expand Up @@ -675,6 +681,7 @@ def show_login_screen(self):
backdrop = tk.Canvas(self.root, bg=pick(BG), highlightthickness=0,
bd=0)
backdrop.place(relx=0, rely=0, relwidth=1, relheight=1)
self._watch_backdrop_mode(backdrop)
backdrop.bind("<Configure>",
lambda e: self._backdrop_resized(backdrop, e.width,
e.height))
Expand Down Expand Up @@ -777,6 +784,23 @@ def draw():
self._draw_backdrop(canvas, w, h)
self._backdrop_job = self.root.after(BACKDROP_REDRAW_MS, draw)

def _watch_backdrop_mode(self, canvas, seen=None):
"""
The backdrop is a painting, not a widget, so it does not turn light
or dark by itself: look once a second while the login screen shows,
and paint it again when the mode has changed.
"""
self._mode_job = None
if not canvas.winfo_exists():
return
now = ctk.get_appearance_mode()
if seen is not None and now != seen:
canvas.configure(bg=pick(BG))
self._draw_backdrop(canvas, canvas.winfo_width(),
canvas.winfo_height())
self._mode_job = self.root.after(
BACKDROP_MODE_MS, lambda: self._watch_backdrop_mode(canvas, now))

def _draw_backdrop(self, canvas, w, h):
"""
The shapes behind the login card. A Tk canvas draws without
Expand Down Expand Up @@ -1034,12 +1058,13 @@ def go():
# ==================================================================
# Main window
# ==================================================================
def _nav_button(self, parent, text, command):
"""A quiet row of the side bar."""
def _nav_button(self, parent, text, command, picture):
"""A quiet row of the side bar: an icon and a name."""
return ctk.CTkButton(
parent, text=text, command=command, height=38, corner_radius=12,
fg_color="transparent", hover_color=CARD_HOVER, anchor="w",
text_color=MUTED, font=F(13, "bold"))
text_color=MUTED, font=F(13, "bold"),
image=icon(picture, 18, MUTED), compound="left")

def show_main_window(self):
self._clear()
Expand Down Expand Up @@ -1068,7 +1093,8 @@ def show_main_window(self):
# Which vault the list shows. New entries go to the one picked.
self.vault_menu = VaultNav(side, self._on_vault_pick)
self.vault_menu.frame.pack(fill="x")
manage = self._nav_button(side, "Manage vaults…", self.vaults_dialog)
manage = self._nav_button(side, "Manage vaults…", self.vaults_dialog,
"vaults")
manage.pack(fill="x", pady=(6, 0))
Tooltip(manage, "Add, rename, share or delete vaults")

Expand All @@ -1090,25 +1116,28 @@ def show_main_window(self):
pair.pack(fill="x", padx=12, pady=(0, 12))
pair.grid_columnconfigure((0, 1), weight=1, uniform="pair")
syn = self._small_button(pair, "Sync", self.sync_now)
syn.configure(image=icon("sync", 16), compound="left")
syn.grid(row=0, column=0, sticky="ew", padx=(0, 4))
Tooltip(syn, "Sync this account with its server (Settings → Sync)")
lock = self._small_button(pair, "Lock", self.lock_vault)
lock.configure(image=icon("lock", 16), compound="left")
lock.grid(row=0, column=1, sticky="ew", padx=(4, 0))
Tooltip(lock, "Lock the account and sign out")

gear = self._nav_button(side, "Settings", self.settings_dialog)
gear = self._nav_button(side, "Settings", self.settings_dialog,
"settings")
gear.pack(side="bottom", fill="x", pady=(0, 10))
imp = self._nav_button(side, "Import", self.import_dialog)
imp = self._nav_button(side, "Import", self.import_dialog, "import")
imp.pack(side="bottom", fill="x")
Tooltip(imp, "Import a PassMan 2.x vault, or a CSV or JSON file")
gen = self._nav_button(side, "Password generator",
self.generator_dialog)
self.generator_dialog, "key")
gen.pack(side="bottom", fill="x")
Tooltip(gen, "Make a strong password")
# The archive: somewhere you go occasionally; its count tells you
# there is something in it.
self.archive_btn = self._nav_button(side, "Archive",
self.archive_dialog)
self.archive_dialog, "archive")
self.archive_btn.pack(side="bottom", fill="x")
Tooltip(self.archive_btn, "Entries you removed, kept so they can be "
"restored")
Expand Down Expand Up @@ -1460,11 +1489,15 @@ def _show_detail(self):
actions = ctk.CTkFrame(band, fg_color="transparent")
actions.grid(row=0, column=2, padx=(8, 22))
if writable:
self._ghost_button(actions, "Edit", lambda: self.account_dialog(acc),
width=74).pack(side="left", padx=(0, 8))
edit = self._ghost_button(actions, "Edit",
lambda: self.account_dialog(acc),
width=92)
edit.configure(image=icon("edit", 16), compound="left")
edit.pack(side="left", padx=(0, 8))
arch = self._ghost_button(actions, "Archive",
lambda: self._archive_account(acc),
width=88)
width=112)
arch.configure(image=icon("archive", 16), compound="left")
arch.pack(side="left")
Tooltip(arch, "Move to archive — restorable, and it still syncs "
"the removal to your other PCs")
Expand Down
163 changes: 163 additions & 0 deletions sourceFiles/icons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
The app's icons, drawn here.

A dozen line icons on a 24-unit grid, one stroke width, round ends. They are
drawn with Pillow instead of shipped as files, so there is one look, no
licence to carry, and each comes in the light and the dark text colour.
:func:`icon` gives a CustomTkinter image that follows the appearance mode.
"""

import customtkinter as ctk
from PIL import Image, ImageDraw

from theme import TEXT

GRID = 24 # the units the shapes below are written in
STROKE = 1.8 # line width, in those units
OVERSAMPLE = 4 # drawn this much larger, then scaled down: smooth edges
HIDPI = 2 # pixels per point kept in the image, for scaled screens


class _Pen:
"""Draws in grid units onto an oversampled image."""

def __init__(self, pixels, colour):
self.scale = pixels * OVERSAMPLE / GRID
self.colour = colour
self.width = max(1, round(STROKE * self.scale))
self.image = Image.new("RGBA", (pixels * OVERSAMPLE,) * 2, (0, 0, 0, 0))
self.draw = ImageDraw.Draw(self.image)

def _p(self, x, y):
return (x * self.scale, y * self.scale)

def _box(self, x0, y0, x1, y1):
return [x0 * self.scale, y0 * self.scale, x1 * self.scale, y1 * self.scale]

def dot(self, x, y, r=STROKE / 2):
self.draw.ellipse(self._box(x - r, y - r, x + r, y + r), fill=self.colour)

def line(self, *points, closed=False):
"""A stroke through the points, with round ends and joints."""
points = list(points) + ([points[0]] if closed else [])
self.draw.line([self._p(x, y) for x, y in points], fill=self.colour,
width=self.width, joint="curve")
for x, y in points:
self.dot(x, y)

def circle(self, x, y, r):
self.draw.ellipse(self._box(x - r, y - r, x + r, y + r),
outline=self.colour, width=self.width)

def arc(self, x0, y0, x1, y1, start, end):
"""Angles as Pillow has them: clockwise from three o'clock."""
self.draw.arc(self._box(x0, y0, x1, y1), start, end, fill=self.colour,
width=self.width)

def box(self, x0, y0, x1, y1, r=2):
self.draw.rounded_rectangle(self._box(x0, y0, x1, y1),
radius=r * self.scale,
outline=self.colour, width=self.width)


def _plus(p):
p.line((12, 5), (12, 19))
p.line((5, 12), (19, 12))


def _vaults(p):
p.line((12, 3), (3, 8), (12, 13), (21, 8), closed=True)
p.line((3, 13), (12, 18), (21, 13))


def _archive(p):
p.box(3, 4, 21, 9, r=1.5)
p.line((5, 9), (5, 20), (19, 20), (19, 9))
p.line((10, 13.5), (14, 13.5))


def _key(p):
p.circle(8, 16, 4.2)
p.line((11, 13), (20, 4))
p.line((16.5, 7.5), (19, 10))
p.line((19, 5), (21, 7))


def _import(p):
p.line((12, 3.5), (12, 15))
p.line((7, 10), (12, 15), (17, 10))
p.line((4, 20), (20, 20))


def _settings(p):
p.line((4, 7), (12.5, 7))
p.line((19.5, 7), (20, 7))
p.circle(16, 7, 2.6)
p.line((4, 17), (4.5, 17))
p.line((11.5, 17), (20, 17))
p.circle(8, 17, 2.6)


def _sync(p):
p.arc(4, 4, 20, 20, 195, 345)
p.line((20, 4.5), (20, 10), (14.5, 10))
p.arc(4, 4, 20, 20, 15, 165)
p.line((4, 19.5), (4, 14), (9.5, 14))


def _lock(p):
p.box(5, 11, 19, 20.5, r=2.2)
p.arc(8, 3.5, 16, 12.5, 180, 360)
p.line((8, 8), (8, 11))
p.line((16, 8), (16, 11))


def _copy(p):
p.box(9, 9, 20, 20, r=2.2)
p.line((5, 15), (5, 5), (15, 5))


def _edit(p):
p.line((4, 20), (5, 16), (16, 5), (19, 8), (8, 19), closed=True)


def _people(p):
p.circle(9, 8, 3.5)
p.arc(2.5, 14.5, 15.5, 27.5, 180, 360)
p.arc(13, 4.5, 20, 11.5, 270, 90)
p.arc(11.5, 14.8, 21.5, 27, 290, 360)


def _activity(p):
p.line((3, 12), (7, 12), (10, 4.5), (14, 19.5), (17, 12), (21, 12))


SHAPES = {
"plus": _plus, "vaults": _vaults, "archive": _archive, "key": _key,
"import": _import, "settings": _settings, "sync": _sync, "lock": _lock,
"copy": _copy, "edit": _edit, "people": _people, "activity": _activity,
}


def picture(name, colour, pixels):
"""One icon as a Pillow image of ``pixels`` square, in ``colour``."""
pen = _Pen(pixels, colour)
SHAPES[name](pen)
return pen.image.resize((pixels, pixels), Image.LANCZOS)


_cache = {}


def icon(name, size=18, colour=TEXT):
"""
The icon as an image for a CustomTkinter widget: ``size`` points square,
in the pair's light or dark colour, whichever mode is in use.
"""
key = (name, size, tuple(colour))
if key not in _cache:
light, dark = colour
_cache[key] = ctk.CTkImage(
light_image=picture(name, light, size * HIDPI),
dark_image=picture(name, dark, size * HIDPI), size=(size, size))
return _cache[key]
8 changes: 3 additions & 5 deletions sourceFiles/sync_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import sync_client
import vault
from history_ui import local_time
from theme import (F, TEXT, MUTED, GREEN, DANGER, INPUT_BG, BORDER, CARD,
from theme import (F, TEXT, MUTED, GREEN, DANGER, INPUT_BG, CARD,
CARD_HOVER, ACCENT_DEEP, WARN, MONO, tile_colors)
from widgets import (ghost_button, accent_button, small_button, field, note,
check, error_label, dialog_shell)
Expand Down Expand Up @@ -300,8 +300,7 @@ def _conflict_row(parent, index, conflict, answers):
if index == 0:
_secret_labels.clear()

card = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=16,
border_width=1, border_color=BORDER)
card = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=16)
card.grid(row=index + 1, column=0, sticky="ew", pady=6, padx=2)
card.grid_columnconfigure((0, 1), weight=1, uniform="sides")

Expand Down Expand Up @@ -541,8 +540,7 @@ def rebuild():

def _archive_row(app, parent, index, entry, rebuild):
"""A row like the main list's: tile, site, who and when, then actions."""
row = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=14,
border_width=1, border_color=BORDER)
row = ctk.CTkFrame(parent, fg_color=CARD, corner_radius=14)
row.grid(row=index, column=0, sticky="ew", pady=(10 if index == 0 else 4,
4), padx=2)
row.grid_columnconfigure(1, weight=1)
Expand Down
Loading
Loading