Fit the page on startup, name the bundle, show the selection
Three things a first session trips over. The editor opened at an arbitrary zoom. show_page does fit the page, but it runs before the window has been laid out, when the viewport is still its default size; redo it once when the real size arrives. The bundle was named after the PDF, which is whatever the download was called. Take the song's title instead — unsafe characters dropped, then whitespace collapsed to dashes. Accented letters stay, since ä and ö are not a filesystem's problem, but a leading dot would hide the file. And the selected slice was drawn as an outline whose top and bottom edges run under the cut lines painted over them, leaving two thin verticals in the margins and no way to tell what was selected. Wash the slice, as the discard and engraved states already do, and keep the outline for the trim anomalies it exists to show.
This commit is contained in:
+2
-1
@@ -78,7 +78,8 @@ and "Andante" cannot.
|
||||
|
||||
## Export
|
||||
|
||||
**Export bundle…**, choose where the `.zip` goes, done. Inside are the slice
|
||||
**Export bundle…**, choose where the `.zip` goes, done. It is named after the
|
||||
song's title — *Bicycle Race* becomes `Bicycle-Race.zip`. Inside are the slice
|
||||
images in order, their markers, the song metadata, and the original PDF as the
|
||||
archive copy. That zip is the whole interface to noteman; hand it over and open
|
||||
it there.
|
||||
|
||||
@@ -13,6 +13,7 @@ a jump source's `destination`.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -39,6 +40,19 @@ METADATA_FIELDS = (
|
||||
NUMERIC_FIELDS = frozenset({"tempo"})
|
||||
|
||||
|
||||
def filename(project: Project) -> str:
|
||||
"""The bundle's name, from the song's title.
|
||||
|
||||
Spaces become dashes and anything that is not a letter, digit, dash, dot or
|
||||
underscore goes. Letters keep their accents — ä and ö are not a filesystem's
|
||||
problem — but a leading dot would make the bundle invisible.
|
||||
"""
|
||||
# Drop the unsafe characters before collapsing whitespace, not after, or
|
||||
# "Sävel & Ääni" keeps the dash the ampersand left behind.
|
||||
title = re.sub(r"[^\w\s.-]", "", (project.metadata.get("title") or ""))
|
||||
return f"{re.sub(r'\s+', '-', title.strip()).lstrip('.-') or 'song'}.zip"
|
||||
|
||||
|
||||
def _engraving(project: Project, page: int, slot: int) -> dict | None:
|
||||
"""The notation behind a re-engraved slice, or None for a scanned one.
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ def _export(args: argparse.Namespace) -> int:
|
||||
elif project.source_changed():
|
||||
print("WARNING: the PDF has changed since these cuts were made")
|
||||
|
||||
out = Path(args.out) if args.out else source.path.with_suffix(".zip")
|
||||
out = Path(args.out) if args.out else source.path.with_name(bundle.filename(project))
|
||||
try:
|
||||
bundle.write(project, source, out)
|
||||
except ValueError as error:
|
||||
|
||||
@@ -75,6 +75,8 @@ _CUT = QColor(220, 40, 40)
|
||||
_CUT_ACTIVE = QColor(255, 120, 0)
|
||||
_VERTEX = QColor(255, 200, 0)
|
||||
_DISCARD = QColor(120, 120, 140, 90)
|
||||
_SELECT = QColor(0, 170, 0)
|
||||
_SELECT_WASH = QColor(0, 200, 60, 40)
|
||||
_RECT = QColor(40, 140, 220)
|
||||
_MARKER = QColor(150, 60, 190)
|
||||
_ENGRAVED = QColor(200, 120, 0)
|
||||
@@ -104,6 +106,7 @@ class PageView(QGraphicsView):
|
||||
self.selected_slice = 0
|
||||
self.picking = False
|
||||
self._drag: tuple[str, int, int] | None = None
|
||||
self._fitted = False
|
||||
|
||||
# -- state ------------------------------------------------------------
|
||||
|
||||
@@ -137,10 +140,15 @@ class PageView(QGraphicsView):
|
||||
self._slice_polygon(slot, w, h), QPen(Qt.NoPen), QBrush(_DISCARD)
|
||||
)
|
||||
|
||||
# The selected slice, outlined so trim anomalies are visible.
|
||||
pen = QPen(QColor(0, 170, 0), 2)
|
||||
# The selected slice. The outline alone is nearly invisible: its top and
|
||||
# bottom edges run under the cut lines drawn over them, leaving two thin
|
||||
# verticals at the page margins. A wash says which slice is selected at
|
||||
# a glance; the outline stays, because it is what shows trim anomalies.
|
||||
selected = self._slice_polygon(self.selected_slice, w, h)
|
||||
scene.addPolygon(selected, QPen(Qt.NoPen), QBrush(_SELECT_WASH))
|
||||
pen = QPen(_SELECT, 3)
|
||||
pen.setCosmetic(True)
|
||||
scene.addPolygon(self._slice_polygon(self.selected_slice, w, h), pen)
|
||||
scene.addPolygon(selected, pen)
|
||||
|
||||
x0, y0, x1, y1 = self.project.page_content_rect(self.page_index)
|
||||
pen = QPen(_RECT, 2, Qt.DashLine)
|
||||
@@ -361,6 +369,15 @@ class PageView(QGraphicsView):
|
||||
self.redraw()
|
||||
self.changed.emit()
|
||||
|
||||
def resizeEvent(self, event) -> None:
|
||||
super().resizeEvent(event)
|
||||
# The fit in show_page runs before the window has been laid out, when
|
||||
# the viewport is still its default size, so the first page opens at
|
||||
# some arbitrary zoom. Redo it once, when the real size arrives.
|
||||
if not self._fitted and self.pixmap is not None:
|
||||
self._fitted = True
|
||||
self.fitInView(self.scene().sceneRect(), Qt.KeepAspectRatio)
|
||||
|
||||
def wheelEvent(self, event) -> None:
|
||||
factor = 1.15 if event.angleDelta().y() > 0 else 1 / 1.15
|
||||
self.scale(factor, factor)
|
||||
@@ -844,7 +861,10 @@ class Editor(QMainWindow):
|
||||
self.metadata["title"].setFocus()
|
||||
return
|
||||
target, _ = QFileDialog.getSaveFileName(
|
||||
self, "Export bundle", str(self.source.path.with_suffix(".zip")), "Bundle (*.zip)"
|
||||
self,
|
||||
"Export bundle",
|
||||
str(self.source.path.with_name(bundle.filename(self.project))),
|
||||
"Bundle (*.zip)",
|
||||
)
|
||||
if not target:
|
||||
return
|
||||
|
||||
@@ -19,6 +19,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||||
|
||||
from noteman_slicer import bundle # noqa: E402
|
||||
from noteman_slicer.detect import detect_page # noqa: E402
|
||||
from noteman_slicer.editor import Editor # noqa: E402
|
||||
from noteman_slicer.pdf import open_source, page_raster # noqa: E402
|
||||
@@ -105,6 +106,28 @@ def main() -> int:
|
||||
assert reloaded.pages[0].levels == (40, 210)
|
||||
assert [c.points for c in reloaded.pages[0].cuts] == [c.points for c in page.cuts]
|
||||
|
||||
# The page fits the viewport once the window has a real size. show_page's
|
||||
# own fit runs before layout, when the viewport is still its default.
|
||||
editor.resize(900, 700)
|
||||
editor.show()
|
||||
app.processEvents()
|
||||
scene = editor.view.sceneRect()
|
||||
scale = editor.view.transform().m11()
|
||||
viewport = editor.view.viewport()
|
||||
fill = max(
|
||||
scale * scene.width() / viewport.width(),
|
||||
scale * scene.height() / viewport.height(),
|
||||
)
|
||||
# Fit means nearly touching one edge — Qt leaves a small margin of its own.
|
||||
# A "≤ 1" check alone would pass a page zoomed down to a dot.
|
||||
assert 0.9 <= fill <= 1.02, f"page is not fitted to the window: {fill:.3f}"
|
||||
|
||||
# The bundle is named after the song, not the PDF.
|
||||
assert bundle.filename(project) == "Ketun-joululaulu.zip"
|
||||
project.metadata["title"] = "AC/DC: T.N.T. (live)"
|
||||
assert bundle.filename(project) == "ACDC-T.N.T.-live.zip"
|
||||
project.metadata["title"] = "Ketun joululaulu"
|
||||
|
||||
editor.close()
|
||||
source.close()
|
||||
for f in (pdf, saved):
|
||||
|
||||
Reference in New Issue
Block a user