Skip to content

Instantly share code, notes, and snippets.

Last active October 17, 2019 01:09
What would you like to do?
Baby steps towards structured editing. Progress as of 2019/2/25.

Blog 2019/2/25

<- previous | index | next ->

Baby steps towards a structured JSON editor (part 2)

<- part 1 | part 3 ->

Progress report for 2019/2/25

Boxes in boxes, y'all :) I can now render JSON arrays and values.

Here is a render of ["hello", "world", ["these", "are"], "words", "in", "boxes"]

screen shot 2019-02-26 at 3 19 26 am


QtPainter acts like a basic frame buffer: if you want to paint a foreground and background, you must first paint the background and then paint the foreground on top of the background.

For a nested JSON structure, this means we must paint containers before painting their contents, which also means we must know the size of each container before its children are painted.

Currently, this means that for any change to the JSON structure, we have to traverse the entire JSON tree twice. That's obviously not going to scale. We'll need to implement a data structure to track and cache the size of each node, so that changes to the JSON tree need only affect a node and its ancestors.

#!/usr/bin/env python
# a pyqt script which draws words inside of rounded rectangles.
# see
# see
import sys
import time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
json_root = ["hello", "world", ["these", "are"], "words", "in", "boxes"]
word_rgb = (0,0,255)
array_rgb = (0,255,0)
# this function will be called on every paint event.
def on_paint(event, painter):
# fill the canvas with white
painter.fillRect(event.rect(), QBrush(Qt.white))
# paint the words
(x, y) = (20, 20)
paint_json(json_root, painter, x, y)
# recursively paint a json structure, starting at (x,y).
# returns the (width, height) used to paint the word.
def paint_json(js, painter, x, y, pad=5):
if isinstance(js, list):
# first, paint the container box
(w, h) = size_of_json(js, painter, pad)
paint_box(painter, x, y, w, h, array_rgb)
# now paint the elements inside of the box
for j in js:
x += pad
(jw, jh) = size_of_json(j, painter, pad)
paint_json(j, painter, x, y + ((h-jh)/2), pad)
x += jw
return (w, h)
return paint_word_in_box("%s" % js, painter, x, y, word_rgb, pad)
# paint a word inside of a box, starting at (x, y).
# returns the (width, height) used to paint the word.
def paint_word_in_box(text, painter, x, y, rgb, pad=5):
# calculate the size of the text
text_bounds = QFontMetrics(painter.font()).boundingRect(text)
w = pad + text_bounds.width() + pad
h = pad + text_bounds.height() + pad
paint_box(painter, x, y, w, h, rgb)
# configure the text stroke
pen = QPen() # defaults to black
# draw the text
text_top_fudge = -2 # qt text seems to have extra padding on top.
baseline = y + text_bounds.height() + pad + text_top_fudge
pad + x,
return (w, h)
# paint a box (with a border stroke and rounded corners)
def paint_box(painter, x, y, w, h, rgb):
corner_radius = 4
# configure the box stroke
pen = QPen()
(r,g,b) = rgb
# configure the box fill
(r,g,b) = lighter_rgb(rgb)
# draw the box
# note: the rounded corners come out a bit odd.
# see
x, y, w, h,
# return a lighter version of a color
def lighter_rgb(rgb):
(r,g,b) = rgb
return (
min(255, int(r + ((255-r)/1.15))),
min(255, int(g + ((255-g)/1.15))),
min(255, int(b + ((255-b)/1.15))),
# calculate the bounding box size of a json structure
def size_of_json(js, painter, pad=5):
if isinstance(js, list):
total_w = pad
total_h = 0
for j in js:
(w, h) = size_of_json(j, painter, pad)
total_w += (w + pad)
total_h = max(total_h, h)
return (total_w, pad + total_h + pad)
return size_of_word_in_box("%s" % js, painter, pad)
# calculate the bounding box size of a word in a box
def size_of_word_in_box(text, painter, pad=5):
text_bounds = QFontMetrics(painter.font()).boundingRect(text)
w = pad + text_bounds.width() + pad
h = pad + text_bounds.height() + pad
return (w, h)
# a widget which paints words inside of boxes
class WordPainter(QWidget):
def __init__(self, on_paint_fn):
self.on_paint_fn = on_paint_fn
# this gets called every time the widget needs to repaint (e.g. window resize)
def paintEvent(self, event):
then = time.time()
painter = QPainter()
self.on_paint_fn(event, painter)
now = time.time()
elapsed = now - then
print "elapsed: %s" % elapsed
print "fps: %s" % (1.0/elapsed)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = WordPainter(on_paint)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment