vibefpmgui.punch_card

A hidden command (type "punch card" in the Command Palette): the line at the cursor as an IBM punched card, as an 029 keypunch would have punched it.

Fixed-form Fortran's columns come from these cards: 1–5 the statement number, 6 a continuation mark, 7–72 the statement, 73–80 left for a sequence number (so a dropped deck could be sorted again). Each column holds one character, as holes in its rows (12, 11, 0–9, top to bottom): the Hollerith code of the IBM 029 (1964). The 029 had capital letters only, and no [ ] { } ~ ^ \ ` — those columns are left blank.

  1"""A hidden command (type "punch card" in the Command Palette): the line at
  2the cursor as an IBM punched card, as an 029 keypunch would have punched it.
  3
  4Fixed-form Fortran's columns come from these cards: 1–5 the statement
  5number, 6 a continuation mark, 7–72 the statement, 73–80 left for a
  6sequence number (so a dropped deck could be sorted again). Each column
  7holds one character, as holes in its rows (12, 11, 0–9, top to bottom):
  8the Hollerith code of the IBM 029 (1964). The 029 had capital letters
  9only, and no [ ] { } ~ ^ \\ ` — those columns are left blank.
 10"""
 11
 12from __future__ import annotations
 13
 14from qtpy.QtCore import QPointF, QRectF, Qt
 15from qtpy.QtGui import QColor, QFont, QPainter, QPen, QPolygonF
 16from qtpy.QtWidgets import (
 17    QDialog,
 18    QDialogButtonBox,
 19    QLabel,
 20    QSizePolicy,
 21    QVBoxLayout,
 22    QWidget,
 23)
 24
 25COLUMNS = 80
 26ROWS = (12, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9)  # top to bottom
 27TAB = 8  # (a tab: spaces to the next multiple of 8, as a keypunch operator would have typed)
 28
 29# Fortran's fields on the card: (first column, last column, name, short name for a narrow card).
 30FIELDS = ((1, 5, "STATEMENT NUMBER", "STMT NO."), (6, 6, "CONT", "C"), (7, 72, "FORTRAN STATEMENT", "FORTRAN"),
 31          (73, 80, "IDENTIFICATION", "IDENT."))
 32
 33_SPECIAL = {
 34    " ": (), "&": (12,), "-": (11,), "/": (0, 1),
 35    "¢": (12, 2, 8), ".": (12, 3, 8), "<": (12, 4, 8), "(": (12, 5, 8), "+": (12, 6, 8), "|": (12, 7, 8),
 36    "!": (11, 2, 8), "$": (11, 3, 8), "*": (11, 4, 8), ")": (11, 5, 8), ";": (11, 6, 8), "¬": (11, 7, 8),
 37    ",": (0, 3, 8), "%": (0, 4, 8), "_": (0, 5, 8), ">": (0, 6, 8), "?": (0, 7, 8),
 38    ":": (2, 8), "#": (3, 8), "@": (4, 8), "'": (5, 8), "=": (6, 8), '"': (7, 8),
 39}
 40
 41
 42def holes(ch: str) -> tuple[int, ...] | None:
 43    """The rows an IBM 029 punches for a character (upper case: it had no
 44    lower case); None: it can't punch it."""
 45    ch = ch.upper()
 46    if ch.isdigit() and ch.isascii():
 47        return (int(ch),)
 48    if "A" <= ch <= "I":
 49        return 12, ord(ch) - ord("A") + 1
 50    if "J" <= ch <= "R":
 51        return 11, ord(ch) - ord("J") + 1
 52    if "S" <= ch <= "Z":
 53        return 0, ord(ch) - ord("S") + 2
 54    return _SPECIAL.get(ch)
 55
 56
 57def card_text(line: str) -> tuple[str, list[str], str]:
 58    """A line as punched: its first 80 columns (tabs as spaces, upper case),
 59    the characters an 029 couldn't punch (left blank), and what didn't fit."""
 60    expanded = ""
 61    for ch in line:
 62        expanded += " " * (TAB - len(expanded) % TAB) if ch == "\t" else ch
 63    punched, left_off = expanded[:COLUMNS], expanded[COLUMNS:]
 64    unpunchable = sorted({ch for ch in punched if holes(ch) is None})
 65    return punched.upper(), unpunchable, left_off
 66
 67
 68class PunchCard(QWidget):
 69    """The card: cream card stock, the cut corner, each row's digits printed
 70    faintly, the holes, the characters printed along the top edge (as the
 71    029 did), Fortran's fields ruled and named, and the column numbers."""
 72
 73    STOCK = QColor("#efe4c2")
 74    INK = QColor("#2f2a22")
 75    PRINT = QColor(160, 120, 70, 150)  # (the card's own printing: faint)
 76    RULE = QColor(170, 60, 50, 160)    # (Fortran's fields)
 77
 78    def __init__(self, text: str = "", parent=None):
 79        super().__init__(parent)
 80        self.text = text
 81        self.setMinimumSize(760, 340)
 82        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
 83
 84    def set_text(self, text: str):
 85        self.text = text
 86        self.update()
 87
 88    def hole_rects(self) -> list[tuple[int, int, QRectF]]:
 89        """Each hole: (column (1-based), row, where it's drawn)."""
 90        card, column_width, row_height, top = self._layout()
 91        out = []
 92        for i, ch in enumerate(self.text[:COLUMNS]):
 93            for row in holes(ch) or ():
 94                x = card.left() + 0.035 * card.width() + i * column_width
 95                y = top + ROWS.index(row) * row_height
 96                out.append((i + 1, row, QRectF(x + column_width * 0.22, y + row_height * 0.18,
 97                                               column_width * 0.56, row_height * 0.64)))
 98        return out
 99
100    def _layout(self) -> tuple[QRectF, float, float, float]:
101        """The card (7⅜ × 3¼ inches), a column's width, a row's height, row 12's top."""
102        ratio = 7.375 / 3.25
103        width = min(self.width() - 8.0, (self.height() - 8.0) * ratio)
104        card = QRectF((self.width() - width) / 2, (self.height() - width / ratio) / 2, width, width / ratio)
105        column_width = card.width() * 0.93 / COLUMNS
106        top = card.top() + card.height() * 0.13  # (room above row 12 for the printed characters)
107        row_height = card.height() * 0.78 / len(ROWS)  # (and below row 9 for the fields' names)
108        return card, column_width, row_height, top
109
110    def paintEvent(self, _event):
111        painter = QPainter(self)
112        painter.setRenderHint(QPainter.Antialiasing)
113        card, column_width, row_height, top = self._layout()
114        left = card.left() + 0.035 * card.width()
115        # The card, its top-left corner cut (to see at once that a card in the deck is the wrong way round).
116        cut = card.height() * 0.12
117        outline = QPolygonF([QPointF(card.left() + cut, card.top()), card.topRight(), card.bottomRight(),
118                             card.bottomLeft(), QPointF(card.left(), card.top() + cut)])
119        painter.setPen(QPen(QColor(0, 0, 0, 60), 1))
120        painter.setBrush(self.STOCK)
121        painter.drawPolygon(outline)
122        # Fortran's fields: ruled, and named under row 9 (the short name where the long one doesn't fit).
123        small = QFont("Courier New")
124        small.setStyleHint(QFont.TypeWriter)
125        small.setPixelSize(max(5, int(column_width * 0.62)))
126        painter.setFont(small)
127        bottom = top + len(ROWS) * row_height
128        for first, last, name, short in FIELDS:
129            x0, x1 = left + (first - 1) * column_width, left + last * column_width
130            painter.setPen(QPen(self.RULE, 1))
131            if first > 1:
132                painter.drawLine(QPointF(x0, top - row_height * 0.2), QPointF(x0, bottom + row_height * 0.55))
133            label = QRectF(x0, bottom + row_height * 0.05, x1 - x0, row_height * 0.5)
134            fits = [n for n in (name, short) if painter.fontMetrics().horizontalAdvance(n) <= label.width() + 2]
135            if fits:
136                painter.drawText(label, Qt.AlignCenter, fits[0])
137        # Each row's digit printed in every column (rows 0–9), and the column numbers under row 0.
138        painter.setPen(self.PRINT)
139        for row in range(10):
140            y = top + ROWS.index(row) * row_height
141            for column in range(COLUMNS):
142                painter.drawText(QRectF(left + column * column_width, y, column_width, row_height),
143                                 Qt.AlignCenter, str(row))
144        tiny = QFont(small)
145        tiny.setPixelSize(max(4, int(column_width * 0.42)))
146        painter.setFont(tiny)
147        for row in (0,):  # (under row 0: under row 9, the fields' names)
148            y = top + (ROWS.index(row) + 0.72) * row_height
149            for column in range(COLUMNS):
150                painter.drawText(QRectF(left + column * column_width, y, column_width, row_height * 0.4),
151                                 Qt.AlignCenter, str(column + 1))
152        # The holes: the dark behind the card, through it.
153        painter.setPen(Qt.NoPen)
154        painter.setBrush(self.INK)
155        for _column, _row, rect in self.hole_rects():
156            painter.drawRect(rect)
157        # The characters, printed along the top edge as the 029 printed them.
158        typed = QFont(small)
159        typed.setPixelSize(max(6, int(column_width * 0.95)))
160        typed.setBold(True)
161        painter.setFont(typed)
162        painter.setPen(self.INK)
163        for i, ch in enumerate(self.text[:COLUMNS]):
164            if holes(ch):
165                painter.drawText(QRectF(left + i * column_width, card.top() + card.height() * 0.02, column_width,
166                                        card.height() * 0.09), Qt.AlignCenter, ch)
167        painter.end()
168
169
170class PunchCardDialog(QDialog):
171    """The card for a line of a file; ↑ and ↓ go through the file's lines."""
172
173    def __init__(self, lines: list[str], index: int, name: str, parent=None):
174        super().__init__(parent)
175        self.setWindowTitle("Punch Card")
176        self.lines, self.index, self.name = lines, index, name
177        self.card = PunchCard()
178        self.caption = QLabel()
179        self.caption.setWordWrap(True)
180        self.caption.setTextFormat(Qt.RichText)
181        buttons = QDialogButtonBox(QDialogButtonBox.Close)
182        buttons.rejected.connect(self.reject)
183        layout = QVBoxLayout(self)
184        layout.addWidget(self.card, 1)
185        layout.addWidget(self.caption)
186        layout.addWidget(buttons)
187        self.resize(1000, 520)
188        self.show_line(index)
189
190    def show_line(self, index: int):
191        self.index = max(0, min(index, len(self.lines) - 1)) if self.lines else 0
192        line = self.lines[self.index] if self.lines else ""
193        punched, unpunchable, left_off = card_text(line)
194        self.card.set_text(punched)
195        notes = [(f"Line {self.index + 1} of {self.name}, as an IBM 029 keypunch would have punched it "
196                  "(1964: capital letters only; ↑ ↓ for the other lines).")]
197        if unpunchable:
198            notes.append(f"Not on an 029's keyboard, left blank: {' '.join(unpunchable)}")
199        if left_off:
200            notes.append(f"Past column 80, on the next card: {len(left_off)} more characters")
201        self.caption.setText("<br>".join(f"<span style='color:gray'>{_escape(n)}</span>" for n in notes))
202
203    def keyPressEvent(self, event):
204        if event.key() in (Qt.Key_Up, Qt.Key_Down):
205            self.show_line(self.index + (1 if event.key() == Qt.Key_Down else -1))
206            return
207        super().keyPressEvent(event)
208
209
210def _escape(text: str) -> str:
211    return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
COLUMNS = 80
ROWS = (12, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
TAB = 8
FIELDS = ((1, 5, 'STATEMENT NUMBER', 'STMT NO.'), (6, 6, 'CONT', 'C'), (7, 72, 'FORTRAN STATEMENT', 'FORTRAN'), (73, 80, 'IDENTIFICATION', 'IDENT.'))
def holes(ch: str) -> tuple[int, ...] | None:
43def holes(ch: str) -> tuple[int, ...] | None:
44    """The rows an IBM 029 punches for a character (upper case: it had no
45    lower case); None: it can't punch it."""
46    ch = ch.upper()
47    if ch.isdigit() and ch.isascii():
48        return (int(ch),)
49    if "A" <= ch <= "I":
50        return 12, ord(ch) - ord("A") + 1
51    if "J" <= ch <= "R":
52        return 11, ord(ch) - ord("J") + 1
53    if "S" <= ch <= "Z":
54        return 0, ord(ch) - ord("S") + 2
55    return _SPECIAL.get(ch)

The rows an IBM 029 punches for a character (upper case: it had no lower case); None: it can't punch it.

def card_text(line: str) -> tuple[str, list[str], str]:
58def card_text(line: str) -> tuple[str, list[str], str]:
59    """A line as punched: its first 80 columns (tabs as spaces, upper case),
60    the characters an 029 couldn't punch (left blank), and what didn't fit."""
61    expanded = ""
62    for ch in line:
63        expanded += " " * (TAB - len(expanded) % TAB) if ch == "\t" else ch
64    punched, left_off = expanded[:COLUMNS], expanded[COLUMNS:]
65    unpunchable = sorted({ch for ch in punched if holes(ch) is None})
66    return punched.upper(), unpunchable, left_off

A line as punched: its first 80 columns (tabs as spaces, upper case), the characters an 029 couldn't punch (left blank), and what didn't fit.

class PunchCard(PySide6.QtWidgets.QWidget):
 69class PunchCard(QWidget):
 70    """The card: cream card stock, the cut corner, each row's digits printed
 71    faintly, the holes, the characters printed along the top edge (as the
 72    029 did), Fortran's fields ruled and named, and the column numbers."""
 73
 74    STOCK = QColor("#efe4c2")
 75    INK = QColor("#2f2a22")
 76    PRINT = QColor(160, 120, 70, 150)  # (the card's own printing: faint)
 77    RULE = QColor(170, 60, 50, 160)    # (Fortran's fields)
 78
 79    def __init__(self, text: str = "", parent=None):
 80        super().__init__(parent)
 81        self.text = text
 82        self.setMinimumSize(760, 340)
 83        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
 84
 85    def set_text(self, text: str):
 86        self.text = text
 87        self.update()
 88
 89    def hole_rects(self) -> list[tuple[int, int, QRectF]]:
 90        """Each hole: (column (1-based), row, where it's drawn)."""
 91        card, column_width, row_height, top = self._layout()
 92        out = []
 93        for i, ch in enumerate(self.text[:COLUMNS]):
 94            for row in holes(ch) or ():
 95                x = card.left() + 0.035 * card.width() + i * column_width
 96                y = top + ROWS.index(row) * row_height
 97                out.append((i + 1, row, QRectF(x + column_width * 0.22, y + row_height * 0.18,
 98                                               column_width * 0.56, row_height * 0.64)))
 99        return out
100
101    def _layout(self) -> tuple[QRectF, float, float, float]:
102        """The card (7⅜ × 3¼ inches), a column's width, a row's height, row 12's top."""
103        ratio = 7.375 / 3.25
104        width = min(self.width() - 8.0, (self.height() - 8.0) * ratio)
105        card = QRectF((self.width() - width) / 2, (self.height() - width / ratio) / 2, width, width / ratio)
106        column_width = card.width() * 0.93 / COLUMNS
107        top = card.top() + card.height() * 0.13  # (room above row 12 for the printed characters)
108        row_height = card.height() * 0.78 / len(ROWS)  # (and below row 9 for the fields' names)
109        return card, column_width, row_height, top
110
111    def paintEvent(self, _event):
112        painter = QPainter(self)
113        painter.setRenderHint(QPainter.Antialiasing)
114        card, column_width, row_height, top = self._layout()
115        left = card.left() + 0.035 * card.width()
116        # The card, its top-left corner cut (to see at once that a card in the deck is the wrong way round).
117        cut = card.height() * 0.12
118        outline = QPolygonF([QPointF(card.left() + cut, card.top()), card.topRight(), card.bottomRight(),
119                             card.bottomLeft(), QPointF(card.left(), card.top() + cut)])
120        painter.setPen(QPen(QColor(0, 0, 0, 60), 1))
121        painter.setBrush(self.STOCK)
122        painter.drawPolygon(outline)
123        # Fortran's fields: ruled, and named under row 9 (the short name where the long one doesn't fit).
124        small = QFont("Courier New")
125        small.setStyleHint(QFont.TypeWriter)
126        small.setPixelSize(max(5, int(column_width * 0.62)))
127        painter.setFont(small)
128        bottom = top + len(ROWS) * row_height
129        for first, last, name, short in FIELDS:
130            x0, x1 = left + (first - 1) * column_width, left + last * column_width
131            painter.setPen(QPen(self.RULE, 1))
132            if first > 1:
133                painter.drawLine(QPointF(x0, top - row_height * 0.2), QPointF(x0, bottom + row_height * 0.55))
134            label = QRectF(x0, bottom + row_height * 0.05, x1 - x0, row_height * 0.5)
135            fits = [n for n in (name, short) if painter.fontMetrics().horizontalAdvance(n) <= label.width() + 2]
136            if fits:
137                painter.drawText(label, Qt.AlignCenter, fits[0])
138        # Each row's digit printed in every column (rows 0–9), and the column numbers under row 0.
139        painter.setPen(self.PRINT)
140        for row in range(10):
141            y = top + ROWS.index(row) * row_height
142            for column in range(COLUMNS):
143                painter.drawText(QRectF(left + column * column_width, y, column_width, row_height),
144                                 Qt.AlignCenter, str(row))
145        tiny = QFont(small)
146        tiny.setPixelSize(max(4, int(column_width * 0.42)))
147        painter.setFont(tiny)
148        for row in (0,):  # (under row 0: under row 9, the fields' names)
149            y = top + (ROWS.index(row) + 0.72) * row_height
150            for column in range(COLUMNS):
151                painter.drawText(QRectF(left + column * column_width, y, column_width, row_height * 0.4),
152                                 Qt.AlignCenter, str(column + 1))
153        # The holes: the dark behind the card, through it.
154        painter.setPen(Qt.NoPen)
155        painter.setBrush(self.INK)
156        for _column, _row, rect in self.hole_rects():
157            painter.drawRect(rect)
158        # The characters, printed along the top edge as the 029 printed them.
159        typed = QFont(small)
160        typed.setPixelSize(max(6, int(column_width * 0.95)))
161        typed.setBold(True)
162        painter.setFont(typed)
163        painter.setPen(self.INK)
164        for i, ch in enumerate(self.text[:COLUMNS]):
165            if holes(ch):
166                painter.drawText(QRectF(left + i * column_width, card.top() + card.height() * 0.02, column_width,
167                                        card.height() * 0.09), Qt.AlignCenter, ch)
168        painter.end()

The card: cream card stock, the cut corner, each row's digits printed faintly, the holes, the characters printed along the top edge (as the 029 did), Fortran's fields ruled and named, and the column numbers.

PunchCard(text: str = '', parent=None)
79    def __init__(self, text: str = "", parent=None):
80        super().__init__(parent)
81        self.text = text
82        self.setMinimumSize(760, 340)
83        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, modal: bool | None = None, windowModality: PySide6.QtCore.Qt.WindowModality | None = None, enabled: bool | None = None, geometry: PySide6.QtCore.QRect | None = None, frameGeometry: PySide6.QtCore.QRect | None = None, normalGeometry: PySide6.QtCore.QRect | None = None, x: int | None = None, y: int | None = None, pos: PySide6.QtCore.QPoint | None = None, frameSize: PySide6.QtCore.QSize | None = None, size: PySide6.QtCore.QSize | None = None, width: int | None = None, height: int | None = None, rect: PySide6.QtCore.QRect | None = None, childrenRect: PySide6.QtCore.QRect | None = None, childrenRegion: PySide6.QtGui.QRegion | None = None, sizePolicy: PySide6.QtWidgets.QSizePolicy | None = None, minimumSize: PySide6.QtCore.QSize | None = None, maximumSize: PySide6.QtCore.QSize | None = None, minimumWidth: int | None = None, minimumHeight: int | None = None, maximumWidth: int | None = None, maximumHeight: int | None = None, sizeIncrement: PySide6.QtCore.QSize | None = None, baseSize: PySide6.QtCore.QSize | None = None, palette: PySide6.QtGui.QPalette | None = None, font: PySide6.QtGui.QFont | None = None, cursor: PySide6.QtGui.QCursor | None = None, mouseTracking: bool | None = None, tabletTracking: bool | None = None, isActiveWindow: bool | None = None, focusPolicy: PySide6.QtCore.Qt.FocusPolicy | None = None, focus: bool | None = None, contextMenuPolicy: PySide6.QtCore.Qt.ContextMenuPolicy | None = None, updatesEnabled: bool | None = None, visible: bool | None = None, minimized: bool | None = None, maximized: bool | None = None, fullScreen: bool | None = None, sizeHint: PySide6.QtCore.QSize | None = None, minimumSizeHint: PySide6.QtCore.QSize | None = None, acceptDrops: bool | None = None, windowTitle: str | None = None, windowIcon: PySide6.QtGui.QIcon | None = None, windowIconText: str | None = None, windowOpacity: float | None = None, windowModified: bool | None = None, toolTip: str | None = None, toolTipDuration: int | None = None, statusTip: str | None = None, whatsThis: str | None = None, accessibleName: str | None = None, accessibleDescription: str | None = None, accessibleIdentifier: str | None = None, layoutDirection: PySide6.QtCore.Qt.LayoutDirection | None = None, autoFillBackground: bool | None = None, styleSheet: str | None = None, locale: PySide6.QtCore.QLocale | None = None, windowFilePath: str | None = None, inputMethodHints: PySide6.QtCore.Qt.InputMethodHint | None = None) -> None

Initialize self. See help(type(self)) for accurate signature.

STOCK = PySide6.QtGui.QColor.fromRgbF(0.937255, 0.894118, 0.760784, 1.000000)
INK = PySide6.QtGui.QColor.fromRgbF(0.184314, 0.164706, 0.133333, 1.000000)
PRINT = PySide6.QtGui.QColor.fromRgbF(0.627451, 0.470588, 0.274510, 0.588235)
RULE = PySide6.QtGui.QColor.fromRgbF(0.666667, 0.235294, 0.196078, 0.627451)
text
def set_text(self, text: str):
85    def set_text(self, text: str):
86        self.text = text
87        self.update()
def hole_rects(self) -> list[tuple[int, int, PySide6.QtCore.QRectF]]:
89    def hole_rects(self) -> list[tuple[int, int, QRectF]]:
90        """Each hole: (column (1-based), row, where it's drawn)."""
91        card, column_width, row_height, top = self._layout()
92        out = []
93        for i, ch in enumerate(self.text[:COLUMNS]):
94            for row in holes(ch) or ():
95                x = card.left() + 0.035 * card.width() + i * column_width
96                y = top + ROWS.index(row) * row_height
97                out.append((i + 1, row, QRectF(x + column_width * 0.22, y + row_height * 0.18,
98                                               column_width * 0.56, row_height * 0.64)))
99        return out

Each hole: (column (1-based), row, where it's drawn).

def paintEvent(self, _event):
111    def paintEvent(self, _event):
112        painter = QPainter(self)
113        painter.setRenderHint(QPainter.Antialiasing)
114        card, column_width, row_height, top = self._layout()
115        left = card.left() + 0.035 * card.width()
116        # The card, its top-left corner cut (to see at once that a card in the deck is the wrong way round).
117        cut = card.height() * 0.12
118        outline = QPolygonF([QPointF(card.left() + cut, card.top()), card.topRight(), card.bottomRight(),
119                             card.bottomLeft(), QPointF(card.left(), card.top() + cut)])
120        painter.setPen(QPen(QColor(0, 0, 0, 60), 1))
121        painter.setBrush(self.STOCK)
122        painter.drawPolygon(outline)
123        # Fortran's fields: ruled, and named under row 9 (the short name where the long one doesn't fit).
124        small = QFont("Courier New")
125        small.setStyleHint(QFont.TypeWriter)
126        small.setPixelSize(max(5, int(column_width * 0.62)))
127        painter.setFont(small)
128        bottom = top + len(ROWS) * row_height
129        for first, last, name, short in FIELDS:
130            x0, x1 = left + (first - 1) * column_width, left + last * column_width
131            painter.setPen(QPen(self.RULE, 1))
132            if first > 1:
133                painter.drawLine(QPointF(x0, top - row_height * 0.2), QPointF(x0, bottom + row_height * 0.55))
134            label = QRectF(x0, bottom + row_height * 0.05, x1 - x0, row_height * 0.5)
135            fits = [n for n in (name, short) if painter.fontMetrics().horizontalAdvance(n) <= label.width() + 2]
136            if fits:
137                painter.drawText(label, Qt.AlignCenter, fits[0])
138        # Each row's digit printed in every column (rows 0–9), and the column numbers under row 0.
139        painter.setPen(self.PRINT)
140        for row in range(10):
141            y = top + ROWS.index(row) * row_height
142            for column in range(COLUMNS):
143                painter.drawText(QRectF(left + column * column_width, y, column_width, row_height),
144                                 Qt.AlignCenter, str(row))
145        tiny = QFont(small)
146        tiny.setPixelSize(max(4, int(column_width * 0.42)))
147        painter.setFont(tiny)
148        for row in (0,):  # (under row 0: under row 9, the fields' names)
149            y = top + (ROWS.index(row) + 0.72) * row_height
150            for column in range(COLUMNS):
151                painter.drawText(QRectF(left + column * column_width, y, column_width, row_height * 0.4),
152                                 Qt.AlignCenter, str(column + 1))
153        # The holes: the dark behind the card, through it.
154        painter.setPen(Qt.NoPen)
155        painter.setBrush(self.INK)
156        for _column, _row, rect in self.hole_rects():
157            painter.drawRect(rect)
158        # The characters, printed along the top edge as the 029 printed them.
159        typed = QFont(small)
160        typed.setPixelSize(max(6, int(column_width * 0.95)))
161        typed.setBold(True)
162        painter.setFont(typed)
163        painter.setPen(self.INK)
164        for i, ch in enumerate(self.text[:COLUMNS]):
165            if holes(ch):
166                painter.drawText(QRectF(left + i * column_width, card.top() + card.height() * 0.02, column_width,
167                                        card.height() * 0.09), Qt.AlignCenter, ch)
168        painter.end()

paintEvent(self, event: PySide6.QtGui.QPaintEvent, /) -> None

staticMetaObject = PySide6.QtCore.QMetaObject("PunchCard" inherits "QWidget": )
class PunchCardDialog(PySide6.QtWidgets.QDialog):
171class PunchCardDialog(QDialog):
172    """The card for a line of a file; ↑ and ↓ go through the file's lines."""
173
174    def __init__(self, lines: list[str], index: int, name: str, parent=None):
175        super().__init__(parent)
176        self.setWindowTitle("Punch Card")
177        self.lines, self.index, self.name = lines, index, name
178        self.card = PunchCard()
179        self.caption = QLabel()
180        self.caption.setWordWrap(True)
181        self.caption.setTextFormat(Qt.RichText)
182        buttons = QDialogButtonBox(QDialogButtonBox.Close)
183        buttons.rejected.connect(self.reject)
184        layout = QVBoxLayout(self)
185        layout.addWidget(self.card, 1)
186        layout.addWidget(self.caption)
187        layout.addWidget(buttons)
188        self.resize(1000, 520)
189        self.show_line(index)
190
191    def show_line(self, index: int):
192        self.index = max(0, min(index, len(self.lines) - 1)) if self.lines else 0
193        line = self.lines[self.index] if self.lines else ""
194        punched, unpunchable, left_off = card_text(line)
195        self.card.set_text(punched)
196        notes = [(f"Line {self.index + 1} of {self.name}, as an IBM 029 keypunch would have punched it "
197                  "(1964: capital letters only; ↑ ↓ for the other lines).")]
198        if unpunchable:
199            notes.append(f"Not on an 029's keyboard, left blank: {' '.join(unpunchable)}")
200        if left_off:
201            notes.append(f"Past column 80, on the next card: {len(left_off)} more characters")
202        self.caption.setText("<br>".join(f"<span style='color:gray'>{_escape(n)}</span>" for n in notes))
203
204    def keyPressEvent(self, event):
205        if event.key() in (Qt.Key_Up, Qt.Key_Down):
206            self.show_line(self.index + (1 if event.key() == Qt.Key_Down else -1))
207            return
208        super().keyPressEvent(event)

The card for a line of a file; ↑ and ↓ go through the file's lines.

PunchCardDialog(lines: list[str], index: int, name: str, parent=None)
174    def __init__(self, lines: list[str], index: int, name: str, parent=None):
175        super().__init__(parent)
176        self.setWindowTitle("Punch Card")
177        self.lines, self.index, self.name = lines, index, name
178        self.card = PunchCard()
179        self.caption = QLabel()
180        self.caption.setWordWrap(True)
181        self.caption.setTextFormat(Qt.RichText)
182        buttons = QDialogButtonBox(QDialogButtonBox.Close)
183        buttons.rejected.connect(self.reject)
184        layout = QVBoxLayout(self)
185        layout.addWidget(self.card, 1)
186        layout.addWidget(self.caption)
187        layout.addWidget(buttons)
188        self.resize(1000, 520)
189        self.show_line(index)

__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, sizeGripEnabled: bool | None = None, modal: bool | None = None) -> None

Initialize self. See help(type(self)) for accurate signature.

card
caption
def show_line(self, index: int):
191    def show_line(self, index: int):
192        self.index = max(0, min(index, len(self.lines) - 1)) if self.lines else 0
193        line = self.lines[self.index] if self.lines else ""
194        punched, unpunchable, left_off = card_text(line)
195        self.card.set_text(punched)
196        notes = [(f"Line {self.index + 1} of {self.name}, as an IBM 029 keypunch would have punched it "
197                  "(1964: capital letters only; ↑ ↓ for the other lines).")]
198        if unpunchable:
199            notes.append(f"Not on an 029's keyboard, left blank: {' '.join(unpunchable)}")
200        if left_off:
201            notes.append(f"Past column 80, on the next card: {len(left_off)} more characters")
202        self.caption.setText("<br>".join(f"<span style='color:gray'>{_escape(n)}</span>" for n in notes))
def keyPressEvent(self, event):
204    def keyPressEvent(self, event):
205        if event.key() in (Qt.Key_Up, Qt.Key_Down):
206            self.show_line(self.index + (1 if event.key() == Qt.Key_Down else -1))
207            return
208        super().keyPressEvent(event)

keyPressEvent(self, arg__1: PySide6.QtGui.QKeyEvent, /) -> None

staticMetaObject = PySide6.QtCore.QMetaObject("PunchCardDialog" inherits "QDialog": )