feat: Wonder Ink looks like the classroom, and its preview shows the page
The tab strip is drawn from the classroom's own palette, the one the panel, Parent Call and Presentations read: larger titles, an accent under the active tab, worksheets marked with a document icon. Its one corner widget is the lesson's size and Print — not a new-tab button. With one tab the strip stays hidden, as before; a change of size shows a moment's badge instead, and the right-click menu and keys still work. The window is named "Lesson — Wonder Ink" and carries its own icon. The print preview showed "Page 1 could not be shown" in the real session while printing correctly: poppler 25.03 does not write to stdout when asked for "-" — it writes a file called "-.png" into the current directory, which for Wonder Ink is the teacher's home. Pages are now rendered into a directory of their own, and the lab print check fails any preview that does not draw its page. The teacher's manual has lesson size and printing. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
48b3097c43
commit
1cb9fd35d2
+204
-7
@@ -175,6 +175,7 @@ from PyQt6.QtCore import ( # noqa: E402
|
||||
from PyQt6.QtGui import ( # noqa: E402
|
||||
QAction,
|
||||
QGuiApplication,
|
||||
QIcon,
|
||||
QKeySequence,
|
||||
QPageLayout,
|
||||
QPageSize,
|
||||
@@ -202,6 +203,7 @@ from PyQt6.QtWidgets import ( # noqa: E402
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QTabWidget,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
@@ -423,18 +425,36 @@ class WonderInk(QMainWindow):
|
||||
self._level = load_level()
|
||||
|
||||
self.setWindowTitle("Wonder Ink")
|
||||
self.setWindowIcon(QIcon.fromTheme("ekklesia-wonderink"))
|
||||
# On the window rather than the application, so the print preview
|
||||
# (a child of it) matches, and so the lab's checks, which build a
|
||||
# window without main(), see what a teacher sees.
|
||||
self.setStyleSheet(stylesheet())
|
||||
|
||||
self._tabs = QTabWidget(self)
|
||||
self._tabs.setDocumentMode(True)
|
||||
self._tabs.setTabsClosable(True)
|
||||
self._tabs.setMovable(True)
|
||||
# No corner widget. This is where a browser puts its new-tab
|
||||
# button, and the classroom deliberately has nowhere to press.
|
||||
self._tabs.setElideMode(Qt.TextElideMode.ElideRight)
|
||||
self._tabs.tabBar().setExpanding(False)
|
||||
self._tabs.tabBar().setDrawBase(False)
|
||||
self._tabs.tabCloseRequested.connect(self.close_tab)
|
||||
self._tabs.currentChanged.connect(self._current_changed)
|
||||
self.setCentralWidget(self._tabs)
|
||||
|
||||
# The one corner widget: how large the lesson is, and Print. Not
|
||||
# a new-tab button — that is what a browser puts here, and the
|
||||
# classroom deliberately has nowhere to press for one.
|
||||
self._controls = LessonControls(self)
|
||||
self._tabs.setCornerWidget(self._controls, Qt.Corner.TopRightCorner)
|
||||
self.zoom_changed.connect(self._controls.show_level)
|
||||
self._controls.show_level(self._level)
|
||||
|
||||
# What a change of size looks like with the tab strip hidden: a
|
||||
# moment's badge, so Ctrl+Plus visibly did something.
|
||||
self._badge = ZoomBadge(self)
|
||||
self.zoom_changed.connect(self._badge.show_level)
|
||||
|
||||
self._shortcuts()
|
||||
|
||||
# ---- tabs ----------------------------------------------------------
|
||||
@@ -508,7 +528,11 @@ class WonderInk(QMainWindow):
|
||||
# single lesson open looks exactly like the application it
|
||||
# replaced, and the strip appears the moment there is somewhere
|
||||
# to switch to.
|
||||
self._tabs.tabBar().setVisible(self._tabs.count() > 1)
|
||||
self._set_strip(self._tabs.count() > 1)
|
||||
|
||||
def _set_strip(self, visible):
|
||||
self._tabs.tabBar().setVisible(visible)
|
||||
self._controls.setVisible(visible)
|
||||
|
||||
def _retitle(self, view, title):
|
||||
index = self._tabs.indexOf(view)
|
||||
@@ -528,11 +552,16 @@ class WonderInk(QMainWindow):
|
||||
self._tabs.setTabToolTip(index, title)
|
||||
|
||||
if index == self._tabs.currentIndex():
|
||||
self.setWindowTitle(title)
|
||||
self._name_window(title)
|
||||
|
||||
def _reicon(self, view, icon):
|
||||
index = self._tabs.indexOf(view)
|
||||
|
||||
# A worksheet is marked as a worksheet, whatever the engine's
|
||||
# viewer offers as an icon, so it can be told from a lesson page.
|
||||
if view in self._pdf_views:
|
||||
icon = QIcon.fromTheme("application-pdf", QIcon.fromTheme("x-office-document"))
|
||||
|
||||
if index >= 0:
|
||||
self._tabs.setTabIcon(index, icon)
|
||||
|
||||
@@ -540,9 +569,19 @@ class WonderInk(QMainWindow):
|
||||
view = self._tabs.widget(index)
|
||||
|
||||
if view is not None:
|
||||
self.setWindowTitle(view.title() or "Wonder Ink")
|
||||
self._name_window(self._tabs.tabToolTip(index))
|
||||
view.setFocus()
|
||||
|
||||
def _name_window(self, title):
|
||||
# The panel and the window switcher show this. "Wonder Ink" last,
|
||||
# so the application is always recognisable, and never twice.
|
||||
title = (title or "").strip()
|
||||
|
||||
if not title or title == "Wonder Ink":
|
||||
self.setWindowTitle("Wonder Ink")
|
||||
else:
|
||||
self.setWindowTitle(f"{title} — Wonder Ink")
|
||||
|
||||
def current(self):
|
||||
return self._tabs.currentWidget()
|
||||
|
||||
@@ -820,6 +859,7 @@ class WonderInk(QMainWindow):
|
||||
# the PDF on disk, because the engine cannot print its own viewer.
|
||||
self._pdf_views[view] = resolved
|
||||
self._retitle(view, path.stem)
|
||||
self._reicon(view, QIcon())
|
||||
view.load(QUrl.fromLocalFile(str(resolved)))
|
||||
return view
|
||||
|
||||
@@ -952,7 +992,7 @@ class WonderInk(QMainWindow):
|
||||
return
|
||||
|
||||
self._fullscreen_view = self.sender()
|
||||
self._tabs.tabBar().setVisible(False)
|
||||
self._set_strip(False)
|
||||
self.showFullScreen()
|
||||
return
|
||||
|
||||
@@ -987,6 +1027,155 @@ class WonderInk(QMainWindow):
|
||||
self._show_tab_bar()
|
||||
|
||||
|
||||
class LessonControls(QWidget):
|
||||
"""− 100% + and Print, at the end of the tab strip."""
|
||||
|
||||
def __init__(self, window):
|
||||
super().__init__(window)
|
||||
self.setObjectName("controls")
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(8, 4, 10, 4)
|
||||
layout.setSpacing(4)
|
||||
|
||||
self._smaller = self._button("−", "Make the lesson smaller (Ctrl+Minus)", window.zoom_out)
|
||||
self._level = self._button("100%", "Normal size (Ctrl+0)", window.zoom_reset)
|
||||
self._level.setObjectName("level")
|
||||
self._bigger = self._button("+", "Make the lesson bigger (Ctrl+Plus)", window.zoom_in)
|
||||
self._print = self._button("Print", "Print this tab (Ctrl+P)", window.print_current)
|
||||
self._print.setObjectName("print")
|
||||
self._print.setIcon(QIcon.fromTheme("document-print"))
|
||||
self._print.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
||||
|
||||
for widget in (self._smaller, self._level, self._bigger):
|
||||
layout.addWidget(widget)
|
||||
|
||||
layout.addSpacing(10)
|
||||
layout.addWidget(self._print)
|
||||
|
||||
def _button(self, text, tip, handler):
|
||||
button = QToolButton(self)
|
||||
button.setText(text)
|
||||
button.setToolTip(tip)
|
||||
button.setAutoRaise(True)
|
||||
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
button.clicked.connect(handler)
|
||||
return button
|
||||
|
||||
def show_level(self, level):
|
||||
self._level.setText(f"{round(level * 100)}%")
|
||||
self._smaller.setEnabled(level > ZOOM_STEPS[0] + 0.001)
|
||||
self._bigger.setEnabled(level < ZOOM_STEPS[-1] - 0.001)
|
||||
|
||||
|
||||
class ZoomBadge(QLabel):
|
||||
"""The lesson's size, for a moment, top right, after it changes."""
|
||||
|
||||
SHOWN_MS = 1400
|
||||
|
||||
def __init__(self, window):
|
||||
super().__init__(window)
|
||||
self.setObjectName("badge")
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
self.hide()
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.timeout.connect(self.hide)
|
||||
|
||||
def show_level(self, level):
|
||||
self.setText(f"Lesson size {round(level * 100)}%")
|
||||
self.adjustSize()
|
||||
window = self.parentWidget()
|
||||
top = window.centralWidget().geometry().top() + 16
|
||||
|
||||
if window._tabs.tabBar().isVisible():
|
||||
top += window._tabs.tabBar().height()
|
||||
|
||||
self.move(window.width() - self.width() - 24, top)
|
||||
self.raise_()
|
||||
self.show()
|
||||
self._timer.start(self.SHOWN_MS)
|
||||
|
||||
|
||||
def stylesheet():
|
||||
"""The window's look, from the classroom's own palette.
|
||||
|
||||
The same colour scheme the panel, the notifications, Parent Call and
|
||||
the Presentations library draw from, read by classroom_style, so
|
||||
Wonder Ink follows light and dark with the rest of the room. Sizes
|
||||
are logical pixels: Qt scales them by the display scale once.
|
||||
"""
|
||||
try:
|
||||
from classroom_style import load_colours
|
||||
|
||||
c = load_colours()
|
||||
except Exception as error: # a look is never worth not starting
|
||||
say(f"using the default palette: {error}")
|
||||
c = {
|
||||
"window": "#141c20", "view": "#0f171a", "text": "#e6edef",
|
||||
"dim": "#93a5aa", "accent": "#4fb3a8", "on_accent": "#0e1518",
|
||||
"negative": "#e36a6a", "line": "#25333a",
|
||||
}
|
||||
|
||||
return f"""
|
||||
QTabWidget::pane {{ border: none; }}
|
||||
QTabWidget::tab-bar {{ left: 8px; }}
|
||||
QTabBar {{ background: {c['window']}; }}
|
||||
QTabBar::tab {{
|
||||
background: transparent;
|
||||
color: {c['dim']};
|
||||
font-size: 15px;
|
||||
padding: 9px 12px 9px 14px;
|
||||
margin: 6px 2px 0 2px;
|
||||
min-width: 140px;
|
||||
max-width: 280px;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom: 3px solid transparent;
|
||||
}}
|
||||
QTabBar::tab:hover:!selected {{ background: {c['line']}; color: {c['text']}; }}
|
||||
QTabBar::tab:selected {{
|
||||
background: {c['view']};
|
||||
color: {c['text']};
|
||||
font-weight: 600;
|
||||
border-bottom: 3px solid {c['accent']};
|
||||
}}
|
||||
QTabBar::close-button {{ subcontrol-position: right; margin-left: 6px; }}
|
||||
QWidget#controls {{ background: {c['window']}; }}
|
||||
QWidget#controls QToolButton {{
|
||||
color: {c['text']};
|
||||
font-size: 17px;
|
||||
min-width: 34px;
|
||||
min-height: 30px;
|
||||
border-radius: 8px;
|
||||
padding: 2px 8px;
|
||||
}}
|
||||
QWidget#controls QToolButton:hover {{ background: {c['line']}; }}
|
||||
QWidget#controls QToolButton:disabled {{ color: {c['line']}; }}
|
||||
QWidget#controls QToolButton#level {{ font-size: 14px; color: {c['dim']}; min-width: 56px; }}
|
||||
QWidget#controls QToolButton#print {{ font-size: 15px; padding: 2px 12px; }}
|
||||
QLabel#badge {{
|
||||
background: {c['window']};
|
||||
color: {c['text']};
|
||||
border: 1px solid {c['line']};
|
||||
border-radius: 12px;
|
||||
padding: 10px 16px;
|
||||
font-size: 16px;
|
||||
}}
|
||||
QDialog#preview {{ background: {c['window']}; }}
|
||||
QDialog#preview QLabel, QDialog#preview QComboBox, QDialog#preview QSpinBox,
|
||||
QDialog#preview QLineEdit, QDialog#preview QPushButton {{ font-size: 15px; }}
|
||||
QDialog#preview QPushButton {{ min-height: 34px; padding: 4px 18px; border-radius: 8px; }}
|
||||
QDialog#preview QPushButton#print {{
|
||||
background: {c['accent']};
|
||||
color: {c['on_accent']};
|
||||
font-weight: 600;
|
||||
}}
|
||||
QDialog#preview QPushButton#print:disabled {{ background: {c['line']}; color: {c['dim']}; }}
|
||||
QDialog#preview QPushButton#turn {{ min-width: 44px; padding: 4px 8px; font-size: 18px; }}
|
||||
QDialog#preview QLabel#status[error="true"] {{ color: {c['negative']}; }}
|
||||
"""
|
||||
|
||||
|
||||
class _Attempt:
|
||||
"""One request for a PDF. Stale once another is made or the dialog ends.
|
||||
|
||||
@@ -1067,6 +1256,7 @@ class PrintPreview(QDialog):
|
||||
|
||||
def __init__(self, window, title):
|
||||
super().__init__(window)
|
||||
self.setObjectName("preview")
|
||||
self.setWindowTitle(f"Print — {title}")
|
||||
self.setModal(True)
|
||||
|
||||
@@ -1090,6 +1280,8 @@ class PrintPreview(QDialog):
|
||||
self._view = PageView(self)
|
||||
self._previous = QPushButton("‹", self)
|
||||
self._next = QPushButton("›", self)
|
||||
self._previous.setObjectName("turn")
|
||||
self._next.setObjectName("turn")
|
||||
self._where = QLabel(self)
|
||||
self._where.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self._previous.clicked.connect(lambda: self._turn(-1))
|
||||
@@ -1128,10 +1320,12 @@ class PrintPreview(QDialog):
|
||||
form.addRow(self._colour_label, self._colour)
|
||||
|
||||
self._status = QLabel(self)
|
||||
self._status.setObjectName("status")
|
||||
self._status.setWordWrap(True)
|
||||
|
||||
self._cancel = QPushButton("Cancel", self)
|
||||
self._print = QPushButton("Print", self)
|
||||
self._print.setObjectName("print")
|
||||
self._print.setDefault(True)
|
||||
self._cancel.clicked.connect(self.reject)
|
||||
self._print.clicked.connect(self._submit)
|
||||
@@ -1264,7 +1458,9 @@ class PrintPreview(QDialog):
|
||||
|
||||
def _say(self, message, error=False):
|
||||
self._status.setText(message)
|
||||
self._status.setStyleSheet("color: #d9534f;" if error else "")
|
||||
self._status.setProperty("error", error)
|
||||
self._status.style().unpolish(self._status)
|
||||
self._status.style().polish(self._status)
|
||||
|
||||
def _which_changed(self, index):
|
||||
self._range.setVisible(index == 1)
|
||||
@@ -1523,6 +1719,7 @@ def main():
|
||||
# no window class, so this is the app id, and it has to agree with
|
||||
# StartupWMClass in ekklesia-wonderink.desktop exactly.
|
||||
app.setDesktopFileName("ekklesia-wonderink")
|
||||
app.setWindowIcon(QIcon.fromTheme("ekklesia-wonderink"))
|
||||
|
||||
profile = build_profile(app)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user