classes.c93_render_layout_panel
1import json 2import os 3import vanilla 4from AppKit import NSOpenPanel, NSFileHandlingPanelOKButton, NSURL 5from typing import Callable 6 7from lib import fonts as fontlib, layout 8from theme import PANEL_SECTION_MARGIN 9from .c92_render_panel_plugin import RenderPanelPlugin 10 11 12class RenderLayoutPanel(RenderPanelPlugin): 13 """RenderView side panel plugin for page size and margin controls.""" 14 15 DEFAULT_PAGE_MARGIN = "5mm" 16 17 def __init__( 18 self, 19 panelWidth: int = 290, 20 pageSizes: list[str] | None = None, 21 statePath: str | None = None, 22 enableFontPicker: bool = False, 23 fontDirectory: str = "/Users/christianjansky/Library/CloudStorage/Dropbox/KOMETA-Typefaces", 24 ): 25 self._panelWidth = panelWidth 26 self._enableFontPicker = enableFontPicker 27 self._fontDirectory = fontDirectory 28 self._pageSizes = pageSizes or [ 29 "A4Landscape", 30 "A4", 31 "A3Landscape", 32 "A3", 33 "screen", 34 ] 35 self._statePath = statePath or os.path.expanduser( 36 "~/Library/Application Support/KOMETA-Draw/render_view_layout_panel.json" 37 ) 38 self._state = { 39 "pageSize": self._pageSizes[0], 40 "pageMargin": self.DEFAULT_PAGE_MARGIN, 41 "bodyPadding": "", 42 "leading": 1.0, 43 "tracking": 0, 44 "fontSizeMode": 0, 45 "fontSize": 48, 46 "fontFitMode": 0, 47 "alignH": "center", 48 "alignV": "center", 49 "debug": False, 50 "invert": False, 51 "flipX": False, 52 "flipY": False, 53 "perforation": False, 54 "perforationRadius": 2.0, 55 "perforationSpacing": 80.0, 56 "perforationCount": 2, 57 "perforationAlignX": "left", 58 "perforationAlignY": "center", 59 "perforationOffsetX": 0.0, 60 "perforationOffsetY": 0.0, 61 "fontFolder": "", 62 "fontPaths": [], 63 "selectedFontPaths": [], 64 "viewMode": 0, 65 "viewDirection": 0, 66 } 67 self._fontRows = [] 68 self._ui = None 69 self._onChange = None 70 self._persistenceEnabled = True 71 self._loadState() 72 73 def getState(self) -> dict: 74 return dict(self._state) 75 76 def build(self, parent, onChange: Callable[..., None]): 77 """Build panel UI in the provided parent group.""" 78 self._onChange = onChange 79 self._ui = vanilla.Group((0, 0, -0, -0)) 80 81 margin = 10 82 labelW = 90 83 sectionMargin = PANEL_SECTION_MARGIN 84 y = margin 85 self._ui.pageSizeLabel = vanilla.TextBox( 86 (margin, y + 3, labelW, 14), 87 "Page Size", 88 sizeStyle="small", 89 ) 90 self._ui.pageSize = vanilla.ComboBox( 91 (margin + labelW, y, -margin, 20), 92 items=self._pageSizes, 93 callback=self._onPageSizeChanged, 94 sizeStyle="small", 95 ) 96 self._ui.pageSize.set(self._state.get("pageSize", self._pageSizes[0])) 97 98 y += 28 99 self._ui.pageMarginLabel = vanilla.TextBox( 100 (margin, y + 3, labelW, 14), 101 "Page Margin", 102 sizeStyle="small", 103 ) 104 self._ui.pageMargin = vanilla.EditText( 105 (margin + labelW, y, -margin, 19), 106 text=str(self._state.get("pageMargin", self.DEFAULT_PAGE_MARGIN)), 107 callback=self._onPageMarginChanged, 108 sizeStyle="small", 109 ) 110 111 y += sectionMargin 112 self._ui.fontPropsSectionLabel = vanilla.TextBox( 113 (margin, y, -margin, 12), "Font Props", alignment="center", sizeStyle="mini" 114 ) 115 116 y += 18 117 self._ui.fontSizeModeLabel = vanilla.TextBox( 118 (margin, y + 3, labelW, 14), 119 "Font Size", 120 sizeStyle="small", 121 ) 122 self._ui.fontSizeMode = vanilla.SegmentedButton( 123 (margin + labelW, y, -margin, 18), 124 [dict(title="Auto"), dict(title="Manual")], 125 callback=self._onFontSizeModeChanged, 126 ) 127 self._ui.fontSizeMode.set(self._state.get("fontSizeMode", 0)) 128 129 y += 28 130 self._ui.fontFitModeLabel = vanilla.TextBox( 131 (margin, y + 3, labelW, 14), 132 "Fit Mode", 133 sizeStyle="small", 134 ) 135 self._ui.fontFitMode = vanilla.PopUpButton( 136 (margin + labelW, y, -margin, 20), 137 items=["Auto", "Preserve Whitespace", "Running Text"], 138 callback=self._onFontFitModeChanged, 139 sizeStyle="small", 140 ) 141 self._ui.fontFitMode.set(self._state.get("fontFitMode", 0)) 142 self._ui.fontSizeManual = vanilla.Slider( 143 (margin + labelW, y, -42, 19), 144 minValue=8, 145 maxValue=256, 146 value=float(self._state.get("fontSize", 48)), 147 callback=self._onFontSizeChanged, 148 continuous=True, 149 sizeStyle="small", 150 ) 151 self._ui.fontSizeValue = vanilla.TextBox( 152 (-38, y + 3, 28, 14), 153 self._formatFontSizeValue(self._state.get("fontSize", 48)), 154 alignment="right", 155 sizeStyle="small", 156 ) 157 self._refreshSizingControls() 158 159 y += 28 160 self._ui.leadingLabel = vanilla.TextBox( 161 (margin, y + 3, labelW, 14), 162 "Leading", 163 sizeStyle="small", 164 ) 165 self._ui.leading = vanilla.Slider( 166 (margin + labelW, y, -42, 19), 167 minValue=0.5, 168 maxValue=1.5, 169 value=float(self._state.get("leading", 1.0)), 170 callback=self._onLeadingChanged, 171 continuous=True, 172 sizeStyle="small", 173 ) 174 self._ui.leadingValue = vanilla.TextBox( 175 (-38, y + 3, 28, 14), 176 self._formatLeadingValue(self._state.get("leading", 1.0)), 177 alignment="right", 178 sizeStyle="small", 179 ) 180 181 y += 28 182 self._ui.trackingLabel = vanilla.TextBox( 183 (margin, y + 3, labelW, 14), 184 "Tracking", 185 sizeStyle="small", 186 ) 187 self._ui.tracking = vanilla.Slider( 188 (margin + labelW, y, -42, 19), 189 minValue=-50, 190 maxValue=50, 191 value=float(self._state.get("tracking", 0)), 192 callback=self._onTrackingChanged, 193 continuous=True, 194 sizeStyle="small", 195 ) 196 self._ui.trackingValue = vanilla.TextBox( 197 (-38, y + 3, 28, 14), 198 self._formatTrackingValue(self._state.get("tracking", 0)), 199 alignment="right", 200 sizeStyle="small", 201 ) 202 203 y += sectionMargin 204 self._ui.alignSectionLabel = vanilla.TextBox( 205 (margin, y, -margin, 12), "Align", alignment="center", sizeStyle="mini" 206 ) 207 y += 18 208 segW = (self._panelWidth - labelW - margin * 2) // 3 209 self._ui.textAlignLabel = vanilla.TextBox( 210 (margin, y + 4, labelW, 14), "Horizontal", sizeStyle="small" 211 ) 212 self._ui.textAlign = vanilla.SegmentedButton( 213 (margin + labelW, y, -margin, 18), 214 [ 215 dict(title="Left", width=segW), 216 dict(title="Center", width=segW), 217 dict(title="Right", width=segW), 218 ], 219 callback=self._onAlignHChanged, 220 ) 221 self._ui.textAlign.set( 222 ["left", "center", "right"].index(self._state.get("alignH", "center")) 223 ) 224 225 y += 24 226 self._ui.yAlignLabel = vanilla.TextBox( 227 (margin, y + 4, labelW, 14), "Vertical", sizeStyle="small" 228 ) 229 self._ui.yAlign = vanilla.SegmentedButton( 230 (margin + labelW, y, -margin, 18), 231 [ 232 dict(title="Top", width=segW), 233 dict(title="Center", width=segW), 234 dict(title="Bottom", width=segW), 235 ], 236 callback=self._onAlignVChanged, 237 ) 238 self._ui.yAlign.set( 239 ["top", "center", "bottom"].index(self._state.get("alignV", "center")) 240 ) 241 242 y += 28 243 self._ui.debugLabel = vanilla.TextBox( 244 (margin, y + 2, labelW, 16), "Debug", sizeStyle="small" 245 ) 246 self._ui.debug = vanilla.CheckBox( 247 (margin + labelW, y, -margin, 16), 248 title="", 249 value=bool(self._state.get("debug", False)), 250 callback=self._onDebugChanged, 251 sizeStyle="small", 252 ) 253 254 y += 24 255 self._ui.invertLabel = vanilla.TextBox( 256 (margin, y + 2, labelW, 16), "Invert", sizeStyle="small" 257 ) 258 self._ui.invert = vanilla.CheckBox( 259 (margin + labelW, y, -margin, 16), 260 title="", 261 value=bool(self._state.get("invert", False)), 262 callback=self._onInvertChanged, 263 sizeStyle="small", 264 ) 265 266 y += 24 267 self._ui.flipLabel = vanilla.TextBox( 268 (margin, y + 2, labelW, 16), "Flip", sizeStyle="small" 269 ) 270 flipSelected = [] 271 if self._state.get("flipX", False): 272 flipSelected.append(0) 273 if self._state.get("flipY", False): 274 flipSelected.append(1) 275 self._ui.flip = vanilla.SegmentedButton( 276 (margin + labelW, y, -margin, 20), 277 segmentDescriptions=[{"title": "X"}, {"title": "Y"}], 278 selectionStyle="any", 279 callback=self._onFlipChanged, 280 sizeStyle="small", 281 ) 282 self._ui.flip.set(flipSelected) 283 284 y += sectionMargin 285 self._ui.perfSectionLabel = vanilla.TextBox( 286 (margin, y, -margin, 12), 287 "Perforation", 288 alignment="center", 289 sizeStyle="mini", 290 ) 291 y += 18 292 self._ui.perfEnableLabel = vanilla.TextBox( 293 (margin, y + 2, labelW, 16), "Enable", sizeStyle="small" 294 ) 295 self._ui.perfEnable = vanilla.CheckBox( 296 (margin + labelW, y, -(margin + 32), 16), 297 title="", 298 value=bool(self._state.get("perforation", False)), 299 callback=self._onPerforationEnableChanged, 300 sizeStyle="small", 301 ) 302 self._ui.perfSettingsButton = vanilla.Button( 303 (-(margin + 28), y - 1, -margin, 18), 304 "⚙", 305 callback=self._onOpenPerforationPopover, 306 sizeStyle="small", 307 ) 308 309 if self._enableFontPicker: 310 y += 32 311 self._ui.fontStatus = vanilla.TextBox( 312 (margin, y, -margin, 12), 313 "No fonts loaded", 314 alignment="center", 315 sizeStyle="mini", 316 ) 317 318 y += 16 319 actionBtnW = 34 320 self._ui.fontSelectionToggle = vanilla.SegmentedButton( 321 (margin, y, -(margin + actionBtnW + 4), 18), 322 [ 323 dict(title="All"), 324 dict(title="None"), 325 dict(title="Replace"), 326 ], 327 sizeStyle="small", 328 selectionStyle="momentary", 329 callback=self._onFontSelectionToggled, 330 ) 331 self._ui.selectFontTypeButton = vanilla.ActionButton( 332 (-margin - actionBtnW, y + 1, -margin, 17), 333 [ 334 dict(title="Upright", callback=self._onSelectFontsByType), 335 dict(title="Italic", callback=self._onSelectFontsByType), 336 dict(title="Text", callback=self._onSelectFontsByType), 337 dict(title="Display", callback=self._onSelectFontsByType), 338 dict(title="Thin", callback=self._onSelectFontsByType), 339 dict(title="Thick", callback=self._onSelectFontsByType), 340 ], 341 sizeStyle="small", 342 ) 343 344 y += 24 345 self._ui.viewModeLabel = vanilla.TextBox( 346 (margin, y + 4, labelW, 14), "View Fonts", sizeStyle="small" 347 ) 348 self._ui.viewMode = vanilla.SegmentedButton( 349 (margin + labelW, y, -margin, 18), 350 [dict(title="Separate"), dict(title="Together")], 351 callback=self._onViewModeChanged, 352 sizeStyle="small", 353 ) 354 self._ui.viewMode.set(self._state.get("viewMode", 0)) 355 y += 26 356 self._ui.viewDirectionLabel = vanilla.TextBox( 357 (margin, y + 4, labelW, 14), "Direction", sizeStyle="small" 358 ) 359 self._ui.viewDirection = vanilla.SegmentedButton( 360 (margin + labelW, y, -margin, 18), 361 [dict(title="Rows"), dict(title="Columns")], 362 callback=self._onViewDirectionChanged, 363 sizeStyle="small", 364 ) 365 self._ui.viewDirection.set(self._state.get("viewDirection", 0)) 366 self._refreshViewModeControls() 367 y += 30 368 self._ui.fontList = vanilla.List( 369 (margin, y, -margin, -margin), 370 [], 371 columnDescriptions=[ 372 { 373 "title": "Selected", 374 "cell": vanilla.CheckBoxListCell(), 375 "key": "Selected", 376 "editable": True, 377 "width": 56, 378 }, 379 {"title": "Font Name", "key": "Font Name", "editable": False}, 380 ], 381 showColumnTitles=True, 382 allowsMultipleSelection=True, 383 editCallback=self._onFontListEdited, 384 ) 385 386 # Prompt on startup if no valid folder is configured. 387 if not os.path.isdir(self._state.get("fontFolder", "")): 388 self._promptForFontFolder(triggerChange=False) 389 self._refreshFontRows() 390 391 parent.body = self._ui 392 393 def _isValidPageSize(self, value: str) -> bool: 394 try: 395 layout.parsePageSize(value) 396 return True 397 except (ValueError, Exception): 398 return False 399 400 def _onPageSizeChanged(self, sender): 401 value = sender.get().strip() 402 self._state["pageSize"] = value 403 self._saveState() 404 if self._isValidPageSize(value): 405 self._triggerChange(immediate=True) 406 407 def _onPageMarginChanged(self, sender): 408 value = sender.get().strip() or "5mm" 409 self._state["pageMargin"] = value 410 self._saveState() 411 self._triggerChange() 412 413 def _onBodyPaddingChanged(self, sender): 414 self._state["bodyPadding"] = sender.get().strip() 415 self._saveState() 416 self._triggerChange() 417 418 def _onTrackingChanged(self, sender): 419 value = int(round(sender.get() / 5) * 5) 420 self._state["tracking"] = value 421 if self._ui is not None and hasattr(self._ui, "trackingValue"): 422 self._ui.trackingValue.set(self._formatTrackingValue(value)) 423 self._saveState() 424 self._triggerChange() 425 426 def _onLeadingChanged(self, sender): 427 value = round(float(sender.get()) / 0.05) * 0.05 428 self._state["leading"] = value 429 if self._ui is not None and hasattr(self._ui, "leadingValue"): 430 self._ui.leadingValue.set(self._formatLeadingValue(value)) 431 self._saveState() 432 self._triggerChange() 433 434 def _onFontFitModeChanged(self, sender): 435 value = sender.get() 436 self._state["fontFitMode"] = value 437 self._saveState() 438 self._triggerChange(immediate=True) 439 440 def _onFontSizeModeChanged(self, sender): 441 value = sender.get() 442 self._state["fontSizeMode"] = value 443 self._refreshSizingControls() 444 self._saveState() 445 self._triggerChange(immediate=True) 446 447 def _onFontSizeChanged(self, sender): 448 value = int(round(sender.get())) 449 self._state["fontSize"] = value 450 if self._ui is not None and hasattr(self._ui, "fontSizeValue"): 451 self._ui.fontSizeValue.set(self._formatFontSizeValue(value)) 452 self._saveState() 453 self._triggerChange() 454 455 def _refreshSizingControls(self): 456 if self._ui is None: 457 return 458 isManual = self._state.get("fontSizeMode", 0) == 1 459 for attr in ("fontFitModeLabel", "fontFitMode"): 460 if hasattr(self._ui, attr): 461 getattr(self._ui, attr).show(not isManual) 462 for attr in ("fontSizeManual", "fontSizeValue"): 463 if hasattr(self._ui, attr): 464 getattr(self._ui, attr).show(isManual) 465 466 def _onAlignHChanged(self, sender): 467 options = ["left", "center", "right"] 468 self._state["alignH"] = options[sender.get()] 469 self._saveState() 470 self._triggerChange(immediate=True) 471 472 def _onAlignVChanged(self, sender): 473 options = ["top", "center", "bottom"] 474 self._state["alignV"] = options[sender.get()] 475 self._saveState() 476 self._triggerChange(immediate=True) 477 478 def _onDebugChanged(self, sender): 479 self._state["debug"] = bool(sender.get()) 480 self._saveState() 481 self._triggerChange(immediate=True) 482 483 def _onInvertChanged(self, sender): 484 self._state["invert"] = bool(sender.get()) 485 self._saveState() 486 self._triggerChange(immediate=True) 487 488 def _onFlipChanged(self, sender): 489 selected = sender.get() 490 self._state["flipX"] = 0 in selected 491 self._state["flipY"] = 1 in selected 492 self._saveState() 493 self._triggerChange(immediate=True) 494 495 def _onPerforationEnableChanged(self, sender): 496 self._state["perforation"] = bool(sender.get()) 497 self._saveState() 498 self._triggerChange(immediate=True) 499 500 def _onOpenPerforationPopover(self, sender): 501 popMargin = 10 502 popLabelW = 60 503 popW = 220 504 rowH = 19 505 rowGap = 8 506 segH = 18 507 segGap = 8 508 py = popMargin 509 510 self._perfPopover = vanilla.Popover((popW, 231)) 511 512 self._perfPopover.radiusLabel = vanilla.TextBox( 513 (popMargin, py + 3, popLabelW, 14), "Radius", sizeStyle="small" 514 ) 515 self._perfPopover.radius = vanilla.EditText( 516 (popMargin + popLabelW, py, -popMargin, rowH), 517 text=str(self._state.get("perforationRadius", 2.0)), 518 callback=self._onPerforationRadiusChanged, 519 sizeStyle="small", 520 ) 521 py += rowH + rowGap 522 523 self._perfPopover.spacingLabel = vanilla.TextBox( 524 (popMargin, py + 3, popLabelW, 14), "Spacing", sizeStyle="small" 525 ) 526 self._perfPopover.spacing = vanilla.EditText( 527 (popMargin + popLabelW, py, -popMargin, rowH), 528 text=str(self._state.get("perforationSpacing", 80.0)), 529 callback=self._onPerforationSpacingChanged, 530 sizeStyle="small", 531 ) 532 py += rowH + rowGap 533 534 self._perfPopover.countLabel = vanilla.TextBox( 535 (popMargin, py + 3, popLabelW, 14), "Count", sizeStyle="small" 536 ) 537 self._perfPopover.count = vanilla.EditText( 538 (popMargin + popLabelW, py, -popMargin, rowH), 539 text=str(self._state.get("perforationCount", 2)), 540 callback=self._onPerforationCountChanged, 541 sizeStyle="small", 542 ) 543 py += rowH + rowGap 544 545 self._perfPopover.offsetXLabel = vanilla.TextBox( 546 (popMargin, py + 3, popLabelW, 14), "Offset X", sizeStyle="small" 547 ) 548 self._perfPopover.offsetX = vanilla.EditText( 549 (popMargin + popLabelW, py, -popMargin, rowH), 550 text=str(self._state.get("perforationOffsetX", 0.0)), 551 callback=self._onPerforationOffsetXChanged, 552 sizeStyle="small", 553 ) 554 py += rowH + rowGap 555 556 self._perfPopover.offsetYLabel = vanilla.TextBox( 557 (popMargin, py + 3, popLabelW, 14), "Offset Y", sizeStyle="small" 558 ) 559 self._perfPopover.offsetY = vanilla.EditText( 560 (popMargin + popLabelW, py, -popMargin, rowH), 561 text=str(self._state.get("perforationOffsetY", 0.0)), 562 callback=self._onPerforationOffsetYChanged, 563 sizeStyle="small", 564 ) 565 py += rowH + rowGap 566 567 segW = (popW - popLabelW - popMargin * 2) // 3 568 self._perfPopover.alignXLabel = vanilla.TextBox( 569 (popMargin, py + 3, popLabelW, 14), "Align H", sizeStyle="small" 570 ) 571 self._perfPopover.alignX = vanilla.SegmentedButton( 572 (popMargin + popLabelW, py, -popMargin, segH), 573 [ 574 dict(title="Left", width=segW), 575 dict(title="Ctr", width=segW), 576 dict(title="Right", width=segW), 577 ], 578 callback=self._onPerforationAlignXChanged, 579 ) 580 self._perfPopover.alignX.set( 581 ["left", "center", "right"].index( 582 self._state.get("perforationAlignX", "left") 583 ) 584 ) 585 py += segH + segGap 586 587 self._perfPopover.alignYLabel = vanilla.TextBox( 588 (popMargin, py + 3, popLabelW, 14), "Align V", sizeStyle="small" 589 ) 590 self._perfPopover.alignY = vanilla.SegmentedButton( 591 (popMargin + popLabelW, py, -popMargin, segH), 592 [ 593 dict(title="Top", width=segW), 594 dict(title="Ctr", width=segW), 595 dict(title="Bot", width=segW), 596 ], 597 callback=self._onPerforationAlignYChanged, 598 ) 599 self._perfPopover.alignY.set( 600 ["top", "center", "bottom"].index( 601 self._state.get("perforationAlignY", "center") 602 ) 603 ) 604 py += segH + segGap 605 606 self._perfPopover.bodyPaddingLabel = vanilla.TextBox( 607 (popMargin, py + 3, popLabelW, 14), "Body Pad", sizeStyle="small" 608 ) 609 self._perfPopover.bodyPadding = vanilla.EditText( 610 (popMargin + popLabelW, py, -popMargin, rowH), 611 text=str(self._state.get("bodyPadding", "")), 612 callback=self._onBodyPaddingChanged, 613 sizeStyle="small", 614 ) 615 616 self._perfPopover.open(parentView=sender) 617 618 def _onPerforationRadiusChanged(self, sender): 619 try: 620 value = max(0.5, float(sender.get())) 621 except ValueError: 622 value = 2.0 623 self._state["perforationRadius"] = value 624 self._saveState() 625 self._triggerChange(immediate=True) 626 627 def _onPerforationSpacingChanged(self, sender): 628 try: 629 value = max(10.0, float(sender.get())) 630 except ValueError: 631 value = 80.0 632 self._state["perforationSpacing"] = value 633 self._saveState() 634 self._triggerChange(immediate=True) 635 636 def _onPerforationCountChanged(self, sender): 637 try: 638 value = max(1, int(sender.get())) 639 except ValueError: 640 value = 2 641 self._state["perforationCount"] = value 642 self._saveState() 643 self._triggerChange(immediate=True) 644 645 def _onPerforationAlignXChanged(self, sender): 646 self._state["perforationAlignX"] = ["left", "center", "right"][sender.get()] 647 self._saveState() 648 self._triggerChange(immediate=True) 649 650 def _onPerforationAlignYChanged(self, sender): 651 self._state["perforationAlignY"] = ["top", "center", "bottom"][sender.get()] 652 self._saveState() 653 self._triggerChange(immediate=True) 654 655 def _onPerforationOffsetXChanged(self, sender): 656 try: 657 value = float(sender.get()) 658 except ValueError: 659 value = 0.0 660 self._state["perforationOffsetX"] = value 661 self._saveState() 662 self._triggerChange(immediate=True) 663 664 def _onPerforationOffsetYChanged(self, sender): 665 try: 666 value = float(sender.get()) 667 except ValueError: 668 value = 0.0 669 self._state["perforationOffsetY"] = value 670 self._saveState() 671 self._triggerChange(immediate=True) 672 673 def _onChooseFontFolder(self, sender): 674 if self._promptForFontFolder(triggerChange=False): 675 self._refreshFontRows() 676 self._saveState() 677 self._triggerChange(immediate=True) 678 679 def _onFontSelectionToggled(self, sender): 680 idx = sender.get() 681 if idx == 0: 682 self._onSelectAllFonts(sender) 683 elif idx == 1: 684 self._onSelectNoFonts(sender) 685 elif idx == 2: 686 self._onChooseFontFolder(sender) 687 688 def _onSelectAllFonts(self, sender): 689 if not self._fontRows: 690 return 691 for row in self._fontRows: 692 row["Selected"] = True 693 self._syncSelectedFromRows() 694 self._writeRowsToUI() 695 self._saveState() 696 self._triggerChange(immediate=True) 697 698 def _onSelectNoFonts(self, sender): 699 if not self._fontRows: 700 return 701 for row in self._fontRows: 702 row["Selected"] = False 703 self._syncSelectedFromRows() 704 self._writeRowsToUI() 705 self._saveState() 706 self._triggerChange(immediate=True) 707 708 def _onSelectFontsByType(self, sender): 709 if not self._fontRows: 710 return 711 fontType = sender.title().lower() 712 for row in self._fontRows: 713 row["Selected"] = fontlib.isFontType(row["Full Name"], fontType) 714 self._syncSelectedFromRows() 715 self._writeRowsToUI() 716 self._saveState() 717 self._triggerChange(immediate=True) 718 719 def _onFontListEdited(self, sender): 720 rows = sender.get() or [] 721 self._fontRows = [dict(row) for row in rows] 722 self._syncSelectedFromRows() 723 self._saveState() 724 self._triggerChange(immediate=True) 725 726 def _onViewModeChanged(self, sender): 727 self._state["viewMode"] = sender.get() 728 self._refreshViewModeControls() 729 self._saveState() 730 self._triggerChange(immediate=True) 731 732 def _onViewDirectionChanged(self, sender): 733 self._state["viewDirection"] = sender.get() 734 self._saveState() 735 self._triggerChange(immediate=True) 736 737 def _refreshViewModeControls(self): 738 if self._ui is None: 739 return 740 is_together = self._state.get("viewMode", 0) == 1 741 for attr in ("viewDirectionLabel", "viewDirection"): 742 if hasattr(self._ui, attr): 743 getattr(self._ui, attr).show(is_together) 744 745 def _promptForFontFolder(self, triggerChange: bool = True) -> bool: 746 panel = NSOpenPanel.openPanel() 747 panel.setCanChooseFiles_(False) 748 panel.setCanChooseDirectories_(True) 749 panel.setAllowsMultipleSelection_(False) 750 if self._fontDirectory: 751 panel.setDirectoryURL_(NSURL.fileURLWithPath_(self._fontDirectory)) 752 if panel.runModal() != NSFileHandlingPanelOKButton: 753 return False 754 folder = panel.URLs()[0].path() 755 if not folder or not os.path.isdir(folder): 756 return False 757 758 self._state["fontFolder"] = folder 759 self._refreshFontRows() 760 self._saveState() 761 if triggerChange: 762 self._triggerChange() 763 return True 764 765 def _refreshFontRows(self): 766 folder = self._state.get("fontFolder", "") 767 if not folder or not os.path.isdir(folder): 768 self._state["fontFolder"] = "" 769 self._state["fontPaths"] = [] 770 self._state["selectedFontPaths"] = [] 771 self._fontRows = [] 772 self._updateFontStatusText() 773 self._writeRowsToUI() 774 return 775 776 fontPaths = fontlib.getFonts(folder) or [] 777 selected = [ 778 path 779 for path in self._state.get("selectedFontPaths", []) 780 if path in fontPaths 781 ] 782 if not selected: 783 defaultFont = fontlib.findFontByNumber(fontPaths, 50) 784 selected = [defaultFont] if defaultFont else fontPaths[:1] 785 786 self._state["fontPaths"] = fontPaths 787 self._state["selectedFontPaths"] = selected 788 self._fontRows = [ 789 { 790 "Selected": path in selected, 791 "Font Name": fontlib.parseNameObject(path), 792 "Full Name": path, 793 } 794 for path in fontPaths 795 ] 796 self._updateFontStatusText() 797 self._writeRowsToUI() 798 799 def _syncSelectedFromRows(self): 800 self._state["fontPaths"] = [row["Full Name"] for row in self._fontRows] 801 self._state["selectedFontPaths"] = [ 802 row["Full Name"] for row in self._fontRows if row.get("Selected") 803 ] 804 self._updateFontStatusText() 805 806 def _updateFontStatusText(self): 807 total = len(self._state.get("fontPaths", [])) 808 selected = len(self._state.get("selectedFontPaths", [])) 809 810 if total == 0: 811 text = "No fonts loaded" 812 else: 813 text = f"{selected}/{total} fonts" 814 815 if self._ui is not None and hasattr(self._ui, "fontStatus"): 816 self._ui.fontStatus.set(text) 817 818 def _writeRowsToUI(self): 819 if self._ui is not None and hasattr(self._ui, "fontList"): 820 self._ui.fontList.set(self._fontRows) 821 822 def _formatTrackingValue(self, value) -> str: 823 return str(int(round(float(value)))) 824 825 def _formatLeadingValue(self, value) -> str: 826 return f"{float(value):.2f}".rstrip("0").rstrip(".") 827 828 def _formatFontSizeValue(self, value) -> str: 829 return str(int(round(float(value)))) 830 831 def _loadState(self): 832 if not os.path.exists(self._statePath): 833 return 834 try: 835 with open(self._statePath, "r", encoding="utf-8") as f: 836 raw = json.load(f) 837 if isinstance(raw, dict): 838 self._state.update(raw) 839 840 try: 841 self._state["tracking"] = int( 842 round(float(self._state.get("tracking", 0))) 843 ) 844 except Exception: 845 self._state["tracking"] = 0 846 847 try: 848 leading = float(self._state.get("leading", 1.0)) 849 self._state["leading"] = min(1.5, max(0.5, leading)) 850 except Exception: 851 self._state["leading"] = 1.0 852 853 try: 854 fontFitMode = int(self._state.get("fontFitMode", 0)) 855 self._state["fontFitMode"] = min(2, max(0, fontFitMode)) 856 except Exception: 857 self._state["fontFitMode"] = 0 858 859 try: 860 fontSizeMode = int(self._state.get("fontSizeMode", 0)) 861 self._state["fontSizeMode"] = min(1, max(0, fontSizeMode)) 862 except Exception: 863 self._state["fontSizeMode"] = 0 864 865 try: 866 fontSize = int(round(float(self._state.get("fontSize", 48)))) 867 self._state["fontSize"] = min(256, max(8, fontSize)) 868 except Exception: 869 self._state["fontSize"] = 48 870 871 try: 872 alignH = self._state.get("alignH", "center") 873 if alignH not in ("left", "center", "right"): 874 alignH = "center" 875 self._state["alignH"] = alignH 876 except Exception: 877 self._state["alignH"] = "center" 878 879 try: 880 alignV = self._state.get("alignV", "center") 881 if alignV not in ("top", "center", "bottom"): 882 alignV = "center" 883 self._state["alignV"] = alignV 884 except Exception: 885 self._state["alignV"] = "center" 886 887 try: 888 self._state["debug"] = bool(self._state.get("debug", False)) 889 except Exception: 890 self._state["debug"] = False 891 892 try: 893 self._state["perforation"] = bool(self._state.get("perforation", False)) 894 except Exception: 895 self._state["perforation"] = False 896 897 try: 898 self._state["perforationRadius"] = max( 899 0.5, float(self._state.get("perforationRadius", 2.0)) 900 ) 901 except Exception: 902 self._state["perforationRadius"] = 2.0 903 904 try: 905 self._state["perforationSpacing"] = max( 906 10.0, float(self._state.get("perforationSpacing", 80.0)) 907 ) 908 except Exception: 909 self._state["perforationSpacing"] = 80.0 910 911 try: 912 self._state["perforationCount"] = max( 913 1, int(self._state.get("perforationCount", 2)) 914 ) 915 except Exception: 916 self._state["perforationCount"] = 2 917 918 try: 919 alignX = self._state.get("perforationAlignX", "left") 920 if alignX not in ("left", "center", "right"): 921 alignX = "left" 922 self._state["perforationAlignX"] = alignX 923 except Exception: 924 self._state["perforationAlignX"] = "left" 925 926 try: 927 alignY = self._state.get("perforationAlignY", "center") 928 if alignY not in ("top", "center", "bottom"): 929 alignY = "center" 930 self._state["perforationAlignY"] = alignY 931 except Exception: 932 self._state["perforationAlignY"] = "center" 933 934 try: 935 self._state["perforationOffsetX"] = float( 936 self._state.get("perforationOffsetX", 0.0) 937 ) 938 except Exception: 939 self._state["perforationOffsetX"] = 0.0 940 941 try: 942 self._state["perforationOffsetY"] = float( 943 self._state.get("perforationOffsetY", 0.0) 944 ) 945 except Exception: 946 self._state["perforationOffsetY"] = 0.0 947 948 folder = self._state.get("fontFolder", "") 949 if not folder or not os.path.isdir(folder): 950 self._state["fontFolder"] = "" 951 self._state["fontPaths"] = [] 952 self._state["selectedFontPaths"] = [] 953 else: 954 self._state["fontPaths"] = [ 955 p 956 for p in self._state.get("fontPaths", []) 957 if isinstance(p, str) and os.path.isfile(p) 958 ] 959 self._state["selectedFontPaths"] = [ 960 p 961 for p in self._state.get("selectedFontPaths", []) 962 if isinstance(p, str) and os.path.isfile(p) 963 ] 964 except Exception: 965 pass 966 967 def getPerforationParams(self, pageIndex: int) -> dict | None: 968 """Return perforation params for a 0-based page index, or None if disabled/skipped. 969 970 Perforations are drawn only on odd pages (1, 3, 5 … in 1-based terms, 971 i.e. page indices 0, 2, 4 … in 0-based terms). The returned dict can 972 be unpacked directly into ``printing.drawPerforationCircles``: 973 974 params = panel.getPerforationParams(pageIndex) 975 if params: 976 printing.drawPerforationCircles(container, **params) 977 """ 978 if not self._state.get("perforation", False): 979 return None 980 if pageIndex % 2 != 0: 981 return None 982 return { 983 "radius": self._state.get("perforationRadius", 2.0), 984 "spacing": self._state.get("perforationSpacing", 80.0), 985 "count": self._state.get("perforationCount", 2), 986 "align": ( 987 self._state.get("perforationAlignX", "left"), 988 self._state.get("perforationAlignY", "center"), 989 ), 990 }
13class RenderLayoutPanel(RenderPanelPlugin): 14 """RenderView side panel plugin for page size and margin controls.""" 15 16 DEFAULT_PAGE_MARGIN = "5mm" 17 18 def __init__( 19 self, 20 panelWidth: int = 290, 21 pageSizes: list[str] | None = None, 22 statePath: str | None = None, 23 enableFontPicker: bool = False, 24 fontDirectory: str = "/Users/christianjansky/Library/CloudStorage/Dropbox/KOMETA-Typefaces", 25 ): 26 self._panelWidth = panelWidth 27 self._enableFontPicker = enableFontPicker 28 self._fontDirectory = fontDirectory 29 self._pageSizes = pageSizes or [ 30 "A4Landscape", 31 "A4", 32 "A3Landscape", 33 "A3", 34 "screen", 35 ] 36 self._statePath = statePath or os.path.expanduser( 37 "~/Library/Application Support/KOMETA-Draw/render_view_layout_panel.json" 38 ) 39 self._state = { 40 "pageSize": self._pageSizes[0], 41 "pageMargin": self.DEFAULT_PAGE_MARGIN, 42 "bodyPadding": "", 43 "leading": 1.0, 44 "tracking": 0, 45 "fontSizeMode": 0, 46 "fontSize": 48, 47 "fontFitMode": 0, 48 "alignH": "center", 49 "alignV": "center", 50 "debug": False, 51 "invert": False, 52 "flipX": False, 53 "flipY": False, 54 "perforation": False, 55 "perforationRadius": 2.0, 56 "perforationSpacing": 80.0, 57 "perforationCount": 2, 58 "perforationAlignX": "left", 59 "perforationAlignY": "center", 60 "perforationOffsetX": 0.0, 61 "perforationOffsetY": 0.0, 62 "fontFolder": "", 63 "fontPaths": [], 64 "selectedFontPaths": [], 65 "viewMode": 0, 66 "viewDirection": 0, 67 } 68 self._fontRows = [] 69 self._ui = None 70 self._onChange = None 71 self._persistenceEnabled = True 72 self._loadState() 73 74 def getState(self) -> dict: 75 return dict(self._state) 76 77 def build(self, parent, onChange: Callable[..., None]): 78 """Build panel UI in the provided parent group.""" 79 self._onChange = onChange 80 self._ui = vanilla.Group((0, 0, -0, -0)) 81 82 margin = 10 83 labelW = 90 84 sectionMargin = PANEL_SECTION_MARGIN 85 y = margin 86 self._ui.pageSizeLabel = vanilla.TextBox( 87 (margin, y + 3, labelW, 14), 88 "Page Size", 89 sizeStyle="small", 90 ) 91 self._ui.pageSize = vanilla.ComboBox( 92 (margin + labelW, y, -margin, 20), 93 items=self._pageSizes, 94 callback=self._onPageSizeChanged, 95 sizeStyle="small", 96 ) 97 self._ui.pageSize.set(self._state.get("pageSize", self._pageSizes[0])) 98 99 y += 28 100 self._ui.pageMarginLabel = vanilla.TextBox( 101 (margin, y + 3, labelW, 14), 102 "Page Margin", 103 sizeStyle="small", 104 ) 105 self._ui.pageMargin = vanilla.EditText( 106 (margin + labelW, y, -margin, 19), 107 text=str(self._state.get("pageMargin", self.DEFAULT_PAGE_MARGIN)), 108 callback=self._onPageMarginChanged, 109 sizeStyle="small", 110 ) 111 112 y += sectionMargin 113 self._ui.fontPropsSectionLabel = vanilla.TextBox( 114 (margin, y, -margin, 12), "Font Props", alignment="center", sizeStyle="mini" 115 ) 116 117 y += 18 118 self._ui.fontSizeModeLabel = vanilla.TextBox( 119 (margin, y + 3, labelW, 14), 120 "Font Size", 121 sizeStyle="small", 122 ) 123 self._ui.fontSizeMode = vanilla.SegmentedButton( 124 (margin + labelW, y, -margin, 18), 125 [dict(title="Auto"), dict(title="Manual")], 126 callback=self._onFontSizeModeChanged, 127 ) 128 self._ui.fontSizeMode.set(self._state.get("fontSizeMode", 0)) 129 130 y += 28 131 self._ui.fontFitModeLabel = vanilla.TextBox( 132 (margin, y + 3, labelW, 14), 133 "Fit Mode", 134 sizeStyle="small", 135 ) 136 self._ui.fontFitMode = vanilla.PopUpButton( 137 (margin + labelW, y, -margin, 20), 138 items=["Auto", "Preserve Whitespace", "Running Text"], 139 callback=self._onFontFitModeChanged, 140 sizeStyle="small", 141 ) 142 self._ui.fontFitMode.set(self._state.get("fontFitMode", 0)) 143 self._ui.fontSizeManual = vanilla.Slider( 144 (margin + labelW, y, -42, 19), 145 minValue=8, 146 maxValue=256, 147 value=float(self._state.get("fontSize", 48)), 148 callback=self._onFontSizeChanged, 149 continuous=True, 150 sizeStyle="small", 151 ) 152 self._ui.fontSizeValue = vanilla.TextBox( 153 (-38, y + 3, 28, 14), 154 self._formatFontSizeValue(self._state.get("fontSize", 48)), 155 alignment="right", 156 sizeStyle="small", 157 ) 158 self._refreshSizingControls() 159 160 y += 28 161 self._ui.leadingLabel = vanilla.TextBox( 162 (margin, y + 3, labelW, 14), 163 "Leading", 164 sizeStyle="small", 165 ) 166 self._ui.leading = vanilla.Slider( 167 (margin + labelW, y, -42, 19), 168 minValue=0.5, 169 maxValue=1.5, 170 value=float(self._state.get("leading", 1.0)), 171 callback=self._onLeadingChanged, 172 continuous=True, 173 sizeStyle="small", 174 ) 175 self._ui.leadingValue = vanilla.TextBox( 176 (-38, y + 3, 28, 14), 177 self._formatLeadingValue(self._state.get("leading", 1.0)), 178 alignment="right", 179 sizeStyle="small", 180 ) 181 182 y += 28 183 self._ui.trackingLabel = vanilla.TextBox( 184 (margin, y + 3, labelW, 14), 185 "Tracking", 186 sizeStyle="small", 187 ) 188 self._ui.tracking = vanilla.Slider( 189 (margin + labelW, y, -42, 19), 190 minValue=-50, 191 maxValue=50, 192 value=float(self._state.get("tracking", 0)), 193 callback=self._onTrackingChanged, 194 continuous=True, 195 sizeStyle="small", 196 ) 197 self._ui.trackingValue = vanilla.TextBox( 198 (-38, y + 3, 28, 14), 199 self._formatTrackingValue(self._state.get("tracking", 0)), 200 alignment="right", 201 sizeStyle="small", 202 ) 203 204 y += sectionMargin 205 self._ui.alignSectionLabel = vanilla.TextBox( 206 (margin, y, -margin, 12), "Align", alignment="center", sizeStyle="mini" 207 ) 208 y += 18 209 segW = (self._panelWidth - labelW - margin * 2) // 3 210 self._ui.textAlignLabel = vanilla.TextBox( 211 (margin, y + 4, labelW, 14), "Horizontal", sizeStyle="small" 212 ) 213 self._ui.textAlign = vanilla.SegmentedButton( 214 (margin + labelW, y, -margin, 18), 215 [ 216 dict(title="Left", width=segW), 217 dict(title="Center", width=segW), 218 dict(title="Right", width=segW), 219 ], 220 callback=self._onAlignHChanged, 221 ) 222 self._ui.textAlign.set( 223 ["left", "center", "right"].index(self._state.get("alignH", "center")) 224 ) 225 226 y += 24 227 self._ui.yAlignLabel = vanilla.TextBox( 228 (margin, y + 4, labelW, 14), "Vertical", sizeStyle="small" 229 ) 230 self._ui.yAlign = vanilla.SegmentedButton( 231 (margin + labelW, y, -margin, 18), 232 [ 233 dict(title="Top", width=segW), 234 dict(title="Center", width=segW), 235 dict(title="Bottom", width=segW), 236 ], 237 callback=self._onAlignVChanged, 238 ) 239 self._ui.yAlign.set( 240 ["top", "center", "bottom"].index(self._state.get("alignV", "center")) 241 ) 242 243 y += 28 244 self._ui.debugLabel = vanilla.TextBox( 245 (margin, y + 2, labelW, 16), "Debug", sizeStyle="small" 246 ) 247 self._ui.debug = vanilla.CheckBox( 248 (margin + labelW, y, -margin, 16), 249 title="", 250 value=bool(self._state.get("debug", False)), 251 callback=self._onDebugChanged, 252 sizeStyle="small", 253 ) 254 255 y += 24 256 self._ui.invertLabel = vanilla.TextBox( 257 (margin, y + 2, labelW, 16), "Invert", sizeStyle="small" 258 ) 259 self._ui.invert = vanilla.CheckBox( 260 (margin + labelW, y, -margin, 16), 261 title="", 262 value=bool(self._state.get("invert", False)), 263 callback=self._onInvertChanged, 264 sizeStyle="small", 265 ) 266 267 y += 24 268 self._ui.flipLabel = vanilla.TextBox( 269 (margin, y + 2, labelW, 16), "Flip", sizeStyle="small" 270 ) 271 flipSelected = [] 272 if self._state.get("flipX", False): 273 flipSelected.append(0) 274 if self._state.get("flipY", False): 275 flipSelected.append(1) 276 self._ui.flip = vanilla.SegmentedButton( 277 (margin + labelW, y, -margin, 20), 278 segmentDescriptions=[{"title": "X"}, {"title": "Y"}], 279 selectionStyle="any", 280 callback=self._onFlipChanged, 281 sizeStyle="small", 282 ) 283 self._ui.flip.set(flipSelected) 284 285 y += sectionMargin 286 self._ui.perfSectionLabel = vanilla.TextBox( 287 (margin, y, -margin, 12), 288 "Perforation", 289 alignment="center", 290 sizeStyle="mini", 291 ) 292 y += 18 293 self._ui.perfEnableLabel = vanilla.TextBox( 294 (margin, y + 2, labelW, 16), "Enable", sizeStyle="small" 295 ) 296 self._ui.perfEnable = vanilla.CheckBox( 297 (margin + labelW, y, -(margin + 32), 16), 298 title="", 299 value=bool(self._state.get("perforation", False)), 300 callback=self._onPerforationEnableChanged, 301 sizeStyle="small", 302 ) 303 self._ui.perfSettingsButton = vanilla.Button( 304 (-(margin + 28), y - 1, -margin, 18), 305 "⚙", 306 callback=self._onOpenPerforationPopover, 307 sizeStyle="small", 308 ) 309 310 if self._enableFontPicker: 311 y += 32 312 self._ui.fontStatus = vanilla.TextBox( 313 (margin, y, -margin, 12), 314 "No fonts loaded", 315 alignment="center", 316 sizeStyle="mini", 317 ) 318 319 y += 16 320 actionBtnW = 34 321 self._ui.fontSelectionToggle = vanilla.SegmentedButton( 322 (margin, y, -(margin + actionBtnW + 4), 18), 323 [ 324 dict(title="All"), 325 dict(title="None"), 326 dict(title="Replace"), 327 ], 328 sizeStyle="small", 329 selectionStyle="momentary", 330 callback=self._onFontSelectionToggled, 331 ) 332 self._ui.selectFontTypeButton = vanilla.ActionButton( 333 (-margin - actionBtnW, y + 1, -margin, 17), 334 [ 335 dict(title="Upright", callback=self._onSelectFontsByType), 336 dict(title="Italic", callback=self._onSelectFontsByType), 337 dict(title="Text", callback=self._onSelectFontsByType), 338 dict(title="Display", callback=self._onSelectFontsByType), 339 dict(title="Thin", callback=self._onSelectFontsByType), 340 dict(title="Thick", callback=self._onSelectFontsByType), 341 ], 342 sizeStyle="small", 343 ) 344 345 y += 24 346 self._ui.viewModeLabel = vanilla.TextBox( 347 (margin, y + 4, labelW, 14), "View Fonts", sizeStyle="small" 348 ) 349 self._ui.viewMode = vanilla.SegmentedButton( 350 (margin + labelW, y, -margin, 18), 351 [dict(title="Separate"), dict(title="Together")], 352 callback=self._onViewModeChanged, 353 sizeStyle="small", 354 ) 355 self._ui.viewMode.set(self._state.get("viewMode", 0)) 356 y += 26 357 self._ui.viewDirectionLabel = vanilla.TextBox( 358 (margin, y + 4, labelW, 14), "Direction", sizeStyle="small" 359 ) 360 self._ui.viewDirection = vanilla.SegmentedButton( 361 (margin + labelW, y, -margin, 18), 362 [dict(title="Rows"), dict(title="Columns")], 363 callback=self._onViewDirectionChanged, 364 sizeStyle="small", 365 ) 366 self._ui.viewDirection.set(self._state.get("viewDirection", 0)) 367 self._refreshViewModeControls() 368 y += 30 369 self._ui.fontList = vanilla.List( 370 (margin, y, -margin, -margin), 371 [], 372 columnDescriptions=[ 373 { 374 "title": "Selected", 375 "cell": vanilla.CheckBoxListCell(), 376 "key": "Selected", 377 "editable": True, 378 "width": 56, 379 }, 380 {"title": "Font Name", "key": "Font Name", "editable": False}, 381 ], 382 showColumnTitles=True, 383 allowsMultipleSelection=True, 384 editCallback=self._onFontListEdited, 385 ) 386 387 # Prompt on startup if no valid folder is configured. 388 if not os.path.isdir(self._state.get("fontFolder", "")): 389 self._promptForFontFolder(triggerChange=False) 390 self._refreshFontRows() 391 392 parent.body = self._ui 393 394 def _isValidPageSize(self, value: str) -> bool: 395 try: 396 layout.parsePageSize(value) 397 return True 398 except (ValueError, Exception): 399 return False 400 401 def _onPageSizeChanged(self, sender): 402 value = sender.get().strip() 403 self._state["pageSize"] = value 404 self._saveState() 405 if self._isValidPageSize(value): 406 self._triggerChange(immediate=True) 407 408 def _onPageMarginChanged(self, sender): 409 value = sender.get().strip() or "5mm" 410 self._state["pageMargin"] = value 411 self._saveState() 412 self._triggerChange() 413 414 def _onBodyPaddingChanged(self, sender): 415 self._state["bodyPadding"] = sender.get().strip() 416 self._saveState() 417 self._triggerChange() 418 419 def _onTrackingChanged(self, sender): 420 value = int(round(sender.get() / 5) * 5) 421 self._state["tracking"] = value 422 if self._ui is not None and hasattr(self._ui, "trackingValue"): 423 self._ui.trackingValue.set(self._formatTrackingValue(value)) 424 self._saveState() 425 self._triggerChange() 426 427 def _onLeadingChanged(self, sender): 428 value = round(float(sender.get()) / 0.05) * 0.05 429 self._state["leading"] = value 430 if self._ui is not None and hasattr(self._ui, "leadingValue"): 431 self._ui.leadingValue.set(self._formatLeadingValue(value)) 432 self._saveState() 433 self._triggerChange() 434 435 def _onFontFitModeChanged(self, sender): 436 value = sender.get() 437 self._state["fontFitMode"] = value 438 self._saveState() 439 self._triggerChange(immediate=True) 440 441 def _onFontSizeModeChanged(self, sender): 442 value = sender.get() 443 self._state["fontSizeMode"] = value 444 self._refreshSizingControls() 445 self._saveState() 446 self._triggerChange(immediate=True) 447 448 def _onFontSizeChanged(self, sender): 449 value = int(round(sender.get())) 450 self._state["fontSize"] = value 451 if self._ui is not None and hasattr(self._ui, "fontSizeValue"): 452 self._ui.fontSizeValue.set(self._formatFontSizeValue(value)) 453 self._saveState() 454 self._triggerChange() 455 456 def _refreshSizingControls(self): 457 if self._ui is None: 458 return 459 isManual = self._state.get("fontSizeMode", 0) == 1 460 for attr in ("fontFitModeLabel", "fontFitMode"): 461 if hasattr(self._ui, attr): 462 getattr(self._ui, attr).show(not isManual) 463 for attr in ("fontSizeManual", "fontSizeValue"): 464 if hasattr(self._ui, attr): 465 getattr(self._ui, attr).show(isManual) 466 467 def _onAlignHChanged(self, sender): 468 options = ["left", "center", "right"] 469 self._state["alignH"] = options[sender.get()] 470 self._saveState() 471 self._triggerChange(immediate=True) 472 473 def _onAlignVChanged(self, sender): 474 options = ["top", "center", "bottom"] 475 self._state["alignV"] = options[sender.get()] 476 self._saveState() 477 self._triggerChange(immediate=True) 478 479 def _onDebugChanged(self, sender): 480 self._state["debug"] = bool(sender.get()) 481 self._saveState() 482 self._triggerChange(immediate=True) 483 484 def _onInvertChanged(self, sender): 485 self._state["invert"] = bool(sender.get()) 486 self._saveState() 487 self._triggerChange(immediate=True) 488 489 def _onFlipChanged(self, sender): 490 selected = sender.get() 491 self._state["flipX"] = 0 in selected 492 self._state["flipY"] = 1 in selected 493 self._saveState() 494 self._triggerChange(immediate=True) 495 496 def _onPerforationEnableChanged(self, sender): 497 self._state["perforation"] = bool(sender.get()) 498 self._saveState() 499 self._triggerChange(immediate=True) 500 501 def _onOpenPerforationPopover(self, sender): 502 popMargin = 10 503 popLabelW = 60 504 popW = 220 505 rowH = 19 506 rowGap = 8 507 segH = 18 508 segGap = 8 509 py = popMargin 510 511 self._perfPopover = vanilla.Popover((popW, 231)) 512 513 self._perfPopover.radiusLabel = vanilla.TextBox( 514 (popMargin, py + 3, popLabelW, 14), "Radius", sizeStyle="small" 515 ) 516 self._perfPopover.radius = vanilla.EditText( 517 (popMargin + popLabelW, py, -popMargin, rowH), 518 text=str(self._state.get("perforationRadius", 2.0)), 519 callback=self._onPerforationRadiusChanged, 520 sizeStyle="small", 521 ) 522 py += rowH + rowGap 523 524 self._perfPopover.spacingLabel = vanilla.TextBox( 525 (popMargin, py + 3, popLabelW, 14), "Spacing", sizeStyle="small" 526 ) 527 self._perfPopover.spacing = vanilla.EditText( 528 (popMargin + popLabelW, py, -popMargin, rowH), 529 text=str(self._state.get("perforationSpacing", 80.0)), 530 callback=self._onPerforationSpacingChanged, 531 sizeStyle="small", 532 ) 533 py += rowH + rowGap 534 535 self._perfPopover.countLabel = vanilla.TextBox( 536 (popMargin, py + 3, popLabelW, 14), "Count", sizeStyle="small" 537 ) 538 self._perfPopover.count = vanilla.EditText( 539 (popMargin + popLabelW, py, -popMargin, rowH), 540 text=str(self._state.get("perforationCount", 2)), 541 callback=self._onPerforationCountChanged, 542 sizeStyle="small", 543 ) 544 py += rowH + rowGap 545 546 self._perfPopover.offsetXLabel = vanilla.TextBox( 547 (popMargin, py + 3, popLabelW, 14), "Offset X", sizeStyle="small" 548 ) 549 self._perfPopover.offsetX = vanilla.EditText( 550 (popMargin + popLabelW, py, -popMargin, rowH), 551 text=str(self._state.get("perforationOffsetX", 0.0)), 552 callback=self._onPerforationOffsetXChanged, 553 sizeStyle="small", 554 ) 555 py += rowH + rowGap 556 557 self._perfPopover.offsetYLabel = vanilla.TextBox( 558 (popMargin, py + 3, popLabelW, 14), "Offset Y", sizeStyle="small" 559 ) 560 self._perfPopover.offsetY = vanilla.EditText( 561 (popMargin + popLabelW, py, -popMargin, rowH), 562 text=str(self._state.get("perforationOffsetY", 0.0)), 563 callback=self._onPerforationOffsetYChanged, 564 sizeStyle="small", 565 ) 566 py += rowH + rowGap 567 568 segW = (popW - popLabelW - popMargin * 2) // 3 569 self._perfPopover.alignXLabel = vanilla.TextBox( 570 (popMargin, py + 3, popLabelW, 14), "Align H", sizeStyle="small" 571 ) 572 self._perfPopover.alignX = vanilla.SegmentedButton( 573 (popMargin + popLabelW, py, -popMargin, segH), 574 [ 575 dict(title="Left", width=segW), 576 dict(title="Ctr", width=segW), 577 dict(title="Right", width=segW), 578 ], 579 callback=self._onPerforationAlignXChanged, 580 ) 581 self._perfPopover.alignX.set( 582 ["left", "center", "right"].index( 583 self._state.get("perforationAlignX", "left") 584 ) 585 ) 586 py += segH + segGap 587 588 self._perfPopover.alignYLabel = vanilla.TextBox( 589 (popMargin, py + 3, popLabelW, 14), "Align V", sizeStyle="small" 590 ) 591 self._perfPopover.alignY = vanilla.SegmentedButton( 592 (popMargin + popLabelW, py, -popMargin, segH), 593 [ 594 dict(title="Top", width=segW), 595 dict(title="Ctr", width=segW), 596 dict(title="Bot", width=segW), 597 ], 598 callback=self._onPerforationAlignYChanged, 599 ) 600 self._perfPopover.alignY.set( 601 ["top", "center", "bottom"].index( 602 self._state.get("perforationAlignY", "center") 603 ) 604 ) 605 py += segH + segGap 606 607 self._perfPopover.bodyPaddingLabel = vanilla.TextBox( 608 (popMargin, py + 3, popLabelW, 14), "Body Pad", sizeStyle="small" 609 ) 610 self._perfPopover.bodyPadding = vanilla.EditText( 611 (popMargin + popLabelW, py, -popMargin, rowH), 612 text=str(self._state.get("bodyPadding", "")), 613 callback=self._onBodyPaddingChanged, 614 sizeStyle="small", 615 ) 616 617 self._perfPopover.open(parentView=sender) 618 619 def _onPerforationRadiusChanged(self, sender): 620 try: 621 value = max(0.5, float(sender.get())) 622 except ValueError: 623 value = 2.0 624 self._state["perforationRadius"] = value 625 self._saveState() 626 self._triggerChange(immediate=True) 627 628 def _onPerforationSpacingChanged(self, sender): 629 try: 630 value = max(10.0, float(sender.get())) 631 except ValueError: 632 value = 80.0 633 self._state["perforationSpacing"] = value 634 self._saveState() 635 self._triggerChange(immediate=True) 636 637 def _onPerforationCountChanged(self, sender): 638 try: 639 value = max(1, int(sender.get())) 640 except ValueError: 641 value = 2 642 self._state["perforationCount"] = value 643 self._saveState() 644 self._triggerChange(immediate=True) 645 646 def _onPerforationAlignXChanged(self, sender): 647 self._state["perforationAlignX"] = ["left", "center", "right"][sender.get()] 648 self._saveState() 649 self._triggerChange(immediate=True) 650 651 def _onPerforationAlignYChanged(self, sender): 652 self._state["perforationAlignY"] = ["top", "center", "bottom"][sender.get()] 653 self._saveState() 654 self._triggerChange(immediate=True) 655 656 def _onPerforationOffsetXChanged(self, sender): 657 try: 658 value = float(sender.get()) 659 except ValueError: 660 value = 0.0 661 self._state["perforationOffsetX"] = value 662 self._saveState() 663 self._triggerChange(immediate=True) 664 665 def _onPerforationOffsetYChanged(self, sender): 666 try: 667 value = float(sender.get()) 668 except ValueError: 669 value = 0.0 670 self._state["perforationOffsetY"] = value 671 self._saveState() 672 self._triggerChange(immediate=True) 673 674 def _onChooseFontFolder(self, sender): 675 if self._promptForFontFolder(triggerChange=False): 676 self._refreshFontRows() 677 self._saveState() 678 self._triggerChange(immediate=True) 679 680 def _onFontSelectionToggled(self, sender): 681 idx = sender.get() 682 if idx == 0: 683 self._onSelectAllFonts(sender) 684 elif idx == 1: 685 self._onSelectNoFonts(sender) 686 elif idx == 2: 687 self._onChooseFontFolder(sender) 688 689 def _onSelectAllFonts(self, sender): 690 if not self._fontRows: 691 return 692 for row in self._fontRows: 693 row["Selected"] = True 694 self._syncSelectedFromRows() 695 self._writeRowsToUI() 696 self._saveState() 697 self._triggerChange(immediate=True) 698 699 def _onSelectNoFonts(self, sender): 700 if not self._fontRows: 701 return 702 for row in self._fontRows: 703 row["Selected"] = False 704 self._syncSelectedFromRows() 705 self._writeRowsToUI() 706 self._saveState() 707 self._triggerChange(immediate=True) 708 709 def _onSelectFontsByType(self, sender): 710 if not self._fontRows: 711 return 712 fontType = sender.title().lower() 713 for row in self._fontRows: 714 row["Selected"] = fontlib.isFontType(row["Full Name"], fontType) 715 self._syncSelectedFromRows() 716 self._writeRowsToUI() 717 self._saveState() 718 self._triggerChange(immediate=True) 719 720 def _onFontListEdited(self, sender): 721 rows = sender.get() or [] 722 self._fontRows = [dict(row) for row in rows] 723 self._syncSelectedFromRows() 724 self._saveState() 725 self._triggerChange(immediate=True) 726 727 def _onViewModeChanged(self, sender): 728 self._state["viewMode"] = sender.get() 729 self._refreshViewModeControls() 730 self._saveState() 731 self._triggerChange(immediate=True) 732 733 def _onViewDirectionChanged(self, sender): 734 self._state["viewDirection"] = sender.get() 735 self._saveState() 736 self._triggerChange(immediate=True) 737 738 def _refreshViewModeControls(self): 739 if self._ui is None: 740 return 741 is_together = self._state.get("viewMode", 0) == 1 742 for attr in ("viewDirectionLabel", "viewDirection"): 743 if hasattr(self._ui, attr): 744 getattr(self._ui, attr).show(is_together) 745 746 def _promptForFontFolder(self, triggerChange: bool = True) -> bool: 747 panel = NSOpenPanel.openPanel() 748 panel.setCanChooseFiles_(False) 749 panel.setCanChooseDirectories_(True) 750 panel.setAllowsMultipleSelection_(False) 751 if self._fontDirectory: 752 panel.setDirectoryURL_(NSURL.fileURLWithPath_(self._fontDirectory)) 753 if panel.runModal() != NSFileHandlingPanelOKButton: 754 return False 755 folder = panel.URLs()[0].path() 756 if not folder or not os.path.isdir(folder): 757 return False 758 759 self._state["fontFolder"] = folder 760 self._refreshFontRows() 761 self._saveState() 762 if triggerChange: 763 self._triggerChange() 764 return True 765 766 def _refreshFontRows(self): 767 folder = self._state.get("fontFolder", "") 768 if not folder or not os.path.isdir(folder): 769 self._state["fontFolder"] = "" 770 self._state["fontPaths"] = [] 771 self._state["selectedFontPaths"] = [] 772 self._fontRows = [] 773 self._updateFontStatusText() 774 self._writeRowsToUI() 775 return 776 777 fontPaths = fontlib.getFonts(folder) or [] 778 selected = [ 779 path 780 for path in self._state.get("selectedFontPaths", []) 781 if path in fontPaths 782 ] 783 if not selected: 784 defaultFont = fontlib.findFontByNumber(fontPaths, 50) 785 selected = [defaultFont] if defaultFont else fontPaths[:1] 786 787 self._state["fontPaths"] = fontPaths 788 self._state["selectedFontPaths"] = selected 789 self._fontRows = [ 790 { 791 "Selected": path in selected, 792 "Font Name": fontlib.parseNameObject(path), 793 "Full Name": path, 794 } 795 for path in fontPaths 796 ] 797 self._updateFontStatusText() 798 self._writeRowsToUI() 799 800 def _syncSelectedFromRows(self): 801 self._state["fontPaths"] = [row["Full Name"] for row in self._fontRows] 802 self._state["selectedFontPaths"] = [ 803 row["Full Name"] for row in self._fontRows if row.get("Selected") 804 ] 805 self._updateFontStatusText() 806 807 def _updateFontStatusText(self): 808 total = len(self._state.get("fontPaths", [])) 809 selected = len(self._state.get("selectedFontPaths", [])) 810 811 if total == 0: 812 text = "No fonts loaded" 813 else: 814 text = f"{selected}/{total} fonts" 815 816 if self._ui is not None and hasattr(self._ui, "fontStatus"): 817 self._ui.fontStatus.set(text) 818 819 def _writeRowsToUI(self): 820 if self._ui is not None and hasattr(self._ui, "fontList"): 821 self._ui.fontList.set(self._fontRows) 822 823 def _formatTrackingValue(self, value) -> str: 824 return str(int(round(float(value)))) 825 826 def _formatLeadingValue(self, value) -> str: 827 return f"{float(value):.2f}".rstrip("0").rstrip(".") 828 829 def _formatFontSizeValue(self, value) -> str: 830 return str(int(round(float(value)))) 831 832 def _loadState(self): 833 if not os.path.exists(self._statePath): 834 return 835 try: 836 with open(self._statePath, "r", encoding="utf-8") as f: 837 raw = json.load(f) 838 if isinstance(raw, dict): 839 self._state.update(raw) 840 841 try: 842 self._state["tracking"] = int( 843 round(float(self._state.get("tracking", 0))) 844 ) 845 except Exception: 846 self._state["tracking"] = 0 847 848 try: 849 leading = float(self._state.get("leading", 1.0)) 850 self._state["leading"] = min(1.5, max(0.5, leading)) 851 except Exception: 852 self._state["leading"] = 1.0 853 854 try: 855 fontFitMode = int(self._state.get("fontFitMode", 0)) 856 self._state["fontFitMode"] = min(2, max(0, fontFitMode)) 857 except Exception: 858 self._state["fontFitMode"] = 0 859 860 try: 861 fontSizeMode = int(self._state.get("fontSizeMode", 0)) 862 self._state["fontSizeMode"] = min(1, max(0, fontSizeMode)) 863 except Exception: 864 self._state["fontSizeMode"] = 0 865 866 try: 867 fontSize = int(round(float(self._state.get("fontSize", 48)))) 868 self._state["fontSize"] = min(256, max(8, fontSize)) 869 except Exception: 870 self._state["fontSize"] = 48 871 872 try: 873 alignH = self._state.get("alignH", "center") 874 if alignH not in ("left", "center", "right"): 875 alignH = "center" 876 self._state["alignH"] = alignH 877 except Exception: 878 self._state["alignH"] = "center" 879 880 try: 881 alignV = self._state.get("alignV", "center") 882 if alignV not in ("top", "center", "bottom"): 883 alignV = "center" 884 self._state["alignV"] = alignV 885 except Exception: 886 self._state["alignV"] = "center" 887 888 try: 889 self._state["debug"] = bool(self._state.get("debug", False)) 890 except Exception: 891 self._state["debug"] = False 892 893 try: 894 self._state["perforation"] = bool(self._state.get("perforation", False)) 895 except Exception: 896 self._state["perforation"] = False 897 898 try: 899 self._state["perforationRadius"] = max( 900 0.5, float(self._state.get("perforationRadius", 2.0)) 901 ) 902 except Exception: 903 self._state["perforationRadius"] = 2.0 904 905 try: 906 self._state["perforationSpacing"] = max( 907 10.0, float(self._state.get("perforationSpacing", 80.0)) 908 ) 909 except Exception: 910 self._state["perforationSpacing"] = 80.0 911 912 try: 913 self._state["perforationCount"] = max( 914 1, int(self._state.get("perforationCount", 2)) 915 ) 916 except Exception: 917 self._state["perforationCount"] = 2 918 919 try: 920 alignX = self._state.get("perforationAlignX", "left") 921 if alignX not in ("left", "center", "right"): 922 alignX = "left" 923 self._state["perforationAlignX"] = alignX 924 except Exception: 925 self._state["perforationAlignX"] = "left" 926 927 try: 928 alignY = self._state.get("perforationAlignY", "center") 929 if alignY not in ("top", "center", "bottom"): 930 alignY = "center" 931 self._state["perforationAlignY"] = alignY 932 except Exception: 933 self._state["perforationAlignY"] = "center" 934 935 try: 936 self._state["perforationOffsetX"] = float( 937 self._state.get("perforationOffsetX", 0.0) 938 ) 939 except Exception: 940 self._state["perforationOffsetX"] = 0.0 941 942 try: 943 self._state["perforationOffsetY"] = float( 944 self._state.get("perforationOffsetY", 0.0) 945 ) 946 except Exception: 947 self._state["perforationOffsetY"] = 0.0 948 949 folder = self._state.get("fontFolder", "") 950 if not folder or not os.path.isdir(folder): 951 self._state["fontFolder"] = "" 952 self._state["fontPaths"] = [] 953 self._state["selectedFontPaths"] = [] 954 else: 955 self._state["fontPaths"] = [ 956 p 957 for p in self._state.get("fontPaths", []) 958 if isinstance(p, str) and os.path.isfile(p) 959 ] 960 self._state["selectedFontPaths"] = [ 961 p 962 for p in self._state.get("selectedFontPaths", []) 963 if isinstance(p, str) and os.path.isfile(p) 964 ] 965 except Exception: 966 pass 967 968 def getPerforationParams(self, pageIndex: int) -> dict | None: 969 """Return perforation params for a 0-based page index, or None if disabled/skipped. 970 971 Perforations are drawn only on odd pages (1, 3, 5 … in 1-based terms, 972 i.e. page indices 0, 2, 4 … in 0-based terms). The returned dict can 973 be unpacked directly into ``printing.drawPerforationCircles``: 974 975 params = panel.getPerforationParams(pageIndex) 976 if params: 977 printing.drawPerforationCircles(container, **params) 978 """ 979 if not self._state.get("perforation", False): 980 return None 981 if pageIndex % 2 != 0: 982 return None 983 return { 984 "radius": self._state.get("perforationRadius", 2.0), 985 "spacing": self._state.get("perforationSpacing", 80.0), 986 "count": self._state.get("perforationCount", 2), 987 "align": ( 988 self._state.get("perforationAlignX", "left"), 989 self._state.get("perforationAlignY", "center"), 990 ), 991 }
RenderView side panel plugin for page size and margin controls.
RenderLayoutPanel( panelWidth: int = 290, pageSizes: list[str] | None = None, statePath: str | None = None, enableFontPicker: bool = False, fontDirectory: str = '/Users/christianjansky/Library/CloudStorage/Dropbox/KOMETA-Typefaces')
18 def __init__( 19 self, 20 panelWidth: int = 290, 21 pageSizes: list[str] | None = None, 22 statePath: str | None = None, 23 enableFontPicker: bool = False, 24 fontDirectory: str = "/Users/christianjansky/Library/CloudStorage/Dropbox/KOMETA-Typefaces", 25 ): 26 self._panelWidth = panelWidth 27 self._enableFontPicker = enableFontPicker 28 self._fontDirectory = fontDirectory 29 self._pageSizes = pageSizes or [ 30 "A4Landscape", 31 "A4", 32 "A3Landscape", 33 "A3", 34 "screen", 35 ] 36 self._statePath = statePath or os.path.expanduser( 37 "~/Library/Application Support/KOMETA-Draw/render_view_layout_panel.json" 38 ) 39 self._state = { 40 "pageSize": self._pageSizes[0], 41 "pageMargin": self.DEFAULT_PAGE_MARGIN, 42 "bodyPadding": "", 43 "leading": 1.0, 44 "tracking": 0, 45 "fontSizeMode": 0, 46 "fontSize": 48, 47 "fontFitMode": 0, 48 "alignH": "center", 49 "alignV": "center", 50 "debug": False, 51 "invert": False, 52 "flipX": False, 53 "flipY": False, 54 "perforation": False, 55 "perforationRadius": 2.0, 56 "perforationSpacing": 80.0, 57 "perforationCount": 2, 58 "perforationAlignX": "left", 59 "perforationAlignY": "center", 60 "perforationOffsetX": 0.0, 61 "perforationOffsetY": 0.0, 62 "fontFolder": "", 63 "fontPaths": [], 64 "selectedFontPaths": [], 65 "viewMode": 0, 66 "viewDirection": 0, 67 } 68 self._fontRows = [] 69 self._ui = None 70 self._onChange = None 71 self._persistenceEnabled = True 72 self._loadState()
def
build(self, parent, onChange: Callable[..., NoneType]):
77 def build(self, parent, onChange: Callable[..., None]): 78 """Build panel UI in the provided parent group.""" 79 self._onChange = onChange 80 self._ui = vanilla.Group((0, 0, -0, -0)) 81 82 margin = 10 83 labelW = 90 84 sectionMargin = PANEL_SECTION_MARGIN 85 y = margin 86 self._ui.pageSizeLabel = vanilla.TextBox( 87 (margin, y + 3, labelW, 14), 88 "Page Size", 89 sizeStyle="small", 90 ) 91 self._ui.pageSize = vanilla.ComboBox( 92 (margin + labelW, y, -margin, 20), 93 items=self._pageSizes, 94 callback=self._onPageSizeChanged, 95 sizeStyle="small", 96 ) 97 self._ui.pageSize.set(self._state.get("pageSize", self._pageSizes[0])) 98 99 y += 28 100 self._ui.pageMarginLabel = vanilla.TextBox( 101 (margin, y + 3, labelW, 14), 102 "Page Margin", 103 sizeStyle="small", 104 ) 105 self._ui.pageMargin = vanilla.EditText( 106 (margin + labelW, y, -margin, 19), 107 text=str(self._state.get("pageMargin", self.DEFAULT_PAGE_MARGIN)), 108 callback=self._onPageMarginChanged, 109 sizeStyle="small", 110 ) 111 112 y += sectionMargin 113 self._ui.fontPropsSectionLabel = vanilla.TextBox( 114 (margin, y, -margin, 12), "Font Props", alignment="center", sizeStyle="mini" 115 ) 116 117 y += 18 118 self._ui.fontSizeModeLabel = vanilla.TextBox( 119 (margin, y + 3, labelW, 14), 120 "Font Size", 121 sizeStyle="small", 122 ) 123 self._ui.fontSizeMode = vanilla.SegmentedButton( 124 (margin + labelW, y, -margin, 18), 125 [dict(title="Auto"), dict(title="Manual")], 126 callback=self._onFontSizeModeChanged, 127 ) 128 self._ui.fontSizeMode.set(self._state.get("fontSizeMode", 0)) 129 130 y += 28 131 self._ui.fontFitModeLabel = vanilla.TextBox( 132 (margin, y + 3, labelW, 14), 133 "Fit Mode", 134 sizeStyle="small", 135 ) 136 self._ui.fontFitMode = vanilla.PopUpButton( 137 (margin + labelW, y, -margin, 20), 138 items=["Auto", "Preserve Whitespace", "Running Text"], 139 callback=self._onFontFitModeChanged, 140 sizeStyle="small", 141 ) 142 self._ui.fontFitMode.set(self._state.get("fontFitMode", 0)) 143 self._ui.fontSizeManual = vanilla.Slider( 144 (margin + labelW, y, -42, 19), 145 minValue=8, 146 maxValue=256, 147 value=float(self._state.get("fontSize", 48)), 148 callback=self._onFontSizeChanged, 149 continuous=True, 150 sizeStyle="small", 151 ) 152 self._ui.fontSizeValue = vanilla.TextBox( 153 (-38, y + 3, 28, 14), 154 self._formatFontSizeValue(self._state.get("fontSize", 48)), 155 alignment="right", 156 sizeStyle="small", 157 ) 158 self._refreshSizingControls() 159 160 y += 28 161 self._ui.leadingLabel = vanilla.TextBox( 162 (margin, y + 3, labelW, 14), 163 "Leading", 164 sizeStyle="small", 165 ) 166 self._ui.leading = vanilla.Slider( 167 (margin + labelW, y, -42, 19), 168 minValue=0.5, 169 maxValue=1.5, 170 value=float(self._state.get("leading", 1.0)), 171 callback=self._onLeadingChanged, 172 continuous=True, 173 sizeStyle="small", 174 ) 175 self._ui.leadingValue = vanilla.TextBox( 176 (-38, y + 3, 28, 14), 177 self._formatLeadingValue(self._state.get("leading", 1.0)), 178 alignment="right", 179 sizeStyle="small", 180 ) 181 182 y += 28 183 self._ui.trackingLabel = vanilla.TextBox( 184 (margin, y + 3, labelW, 14), 185 "Tracking", 186 sizeStyle="small", 187 ) 188 self._ui.tracking = vanilla.Slider( 189 (margin + labelW, y, -42, 19), 190 minValue=-50, 191 maxValue=50, 192 value=float(self._state.get("tracking", 0)), 193 callback=self._onTrackingChanged, 194 continuous=True, 195 sizeStyle="small", 196 ) 197 self._ui.trackingValue = vanilla.TextBox( 198 (-38, y + 3, 28, 14), 199 self._formatTrackingValue(self._state.get("tracking", 0)), 200 alignment="right", 201 sizeStyle="small", 202 ) 203 204 y += sectionMargin 205 self._ui.alignSectionLabel = vanilla.TextBox( 206 (margin, y, -margin, 12), "Align", alignment="center", sizeStyle="mini" 207 ) 208 y += 18 209 segW = (self._panelWidth - labelW - margin * 2) // 3 210 self._ui.textAlignLabel = vanilla.TextBox( 211 (margin, y + 4, labelW, 14), "Horizontal", sizeStyle="small" 212 ) 213 self._ui.textAlign = vanilla.SegmentedButton( 214 (margin + labelW, y, -margin, 18), 215 [ 216 dict(title="Left", width=segW), 217 dict(title="Center", width=segW), 218 dict(title="Right", width=segW), 219 ], 220 callback=self._onAlignHChanged, 221 ) 222 self._ui.textAlign.set( 223 ["left", "center", "right"].index(self._state.get("alignH", "center")) 224 ) 225 226 y += 24 227 self._ui.yAlignLabel = vanilla.TextBox( 228 (margin, y + 4, labelW, 14), "Vertical", sizeStyle="small" 229 ) 230 self._ui.yAlign = vanilla.SegmentedButton( 231 (margin + labelW, y, -margin, 18), 232 [ 233 dict(title="Top", width=segW), 234 dict(title="Center", width=segW), 235 dict(title="Bottom", width=segW), 236 ], 237 callback=self._onAlignVChanged, 238 ) 239 self._ui.yAlign.set( 240 ["top", "center", "bottom"].index(self._state.get("alignV", "center")) 241 ) 242 243 y += 28 244 self._ui.debugLabel = vanilla.TextBox( 245 (margin, y + 2, labelW, 16), "Debug", sizeStyle="small" 246 ) 247 self._ui.debug = vanilla.CheckBox( 248 (margin + labelW, y, -margin, 16), 249 title="", 250 value=bool(self._state.get("debug", False)), 251 callback=self._onDebugChanged, 252 sizeStyle="small", 253 ) 254 255 y += 24 256 self._ui.invertLabel = vanilla.TextBox( 257 (margin, y + 2, labelW, 16), "Invert", sizeStyle="small" 258 ) 259 self._ui.invert = vanilla.CheckBox( 260 (margin + labelW, y, -margin, 16), 261 title="", 262 value=bool(self._state.get("invert", False)), 263 callback=self._onInvertChanged, 264 sizeStyle="small", 265 ) 266 267 y += 24 268 self._ui.flipLabel = vanilla.TextBox( 269 (margin, y + 2, labelW, 16), "Flip", sizeStyle="small" 270 ) 271 flipSelected = [] 272 if self._state.get("flipX", False): 273 flipSelected.append(0) 274 if self._state.get("flipY", False): 275 flipSelected.append(1) 276 self._ui.flip = vanilla.SegmentedButton( 277 (margin + labelW, y, -margin, 20), 278 segmentDescriptions=[{"title": "X"}, {"title": "Y"}], 279 selectionStyle="any", 280 callback=self._onFlipChanged, 281 sizeStyle="small", 282 ) 283 self._ui.flip.set(flipSelected) 284 285 y += sectionMargin 286 self._ui.perfSectionLabel = vanilla.TextBox( 287 (margin, y, -margin, 12), 288 "Perforation", 289 alignment="center", 290 sizeStyle="mini", 291 ) 292 y += 18 293 self._ui.perfEnableLabel = vanilla.TextBox( 294 (margin, y + 2, labelW, 16), "Enable", sizeStyle="small" 295 ) 296 self._ui.perfEnable = vanilla.CheckBox( 297 (margin + labelW, y, -(margin + 32), 16), 298 title="", 299 value=bool(self._state.get("perforation", False)), 300 callback=self._onPerforationEnableChanged, 301 sizeStyle="small", 302 ) 303 self._ui.perfSettingsButton = vanilla.Button( 304 (-(margin + 28), y - 1, -margin, 18), 305 "⚙", 306 callback=self._onOpenPerforationPopover, 307 sizeStyle="small", 308 ) 309 310 if self._enableFontPicker: 311 y += 32 312 self._ui.fontStatus = vanilla.TextBox( 313 (margin, y, -margin, 12), 314 "No fonts loaded", 315 alignment="center", 316 sizeStyle="mini", 317 ) 318 319 y += 16 320 actionBtnW = 34 321 self._ui.fontSelectionToggle = vanilla.SegmentedButton( 322 (margin, y, -(margin + actionBtnW + 4), 18), 323 [ 324 dict(title="All"), 325 dict(title="None"), 326 dict(title="Replace"), 327 ], 328 sizeStyle="small", 329 selectionStyle="momentary", 330 callback=self._onFontSelectionToggled, 331 ) 332 self._ui.selectFontTypeButton = vanilla.ActionButton( 333 (-margin - actionBtnW, y + 1, -margin, 17), 334 [ 335 dict(title="Upright", callback=self._onSelectFontsByType), 336 dict(title="Italic", callback=self._onSelectFontsByType), 337 dict(title="Text", callback=self._onSelectFontsByType), 338 dict(title="Display", callback=self._onSelectFontsByType), 339 dict(title="Thin", callback=self._onSelectFontsByType), 340 dict(title="Thick", callback=self._onSelectFontsByType), 341 ], 342 sizeStyle="small", 343 ) 344 345 y += 24 346 self._ui.viewModeLabel = vanilla.TextBox( 347 (margin, y + 4, labelW, 14), "View Fonts", sizeStyle="small" 348 ) 349 self._ui.viewMode = vanilla.SegmentedButton( 350 (margin + labelW, y, -margin, 18), 351 [dict(title="Separate"), dict(title="Together")], 352 callback=self._onViewModeChanged, 353 sizeStyle="small", 354 ) 355 self._ui.viewMode.set(self._state.get("viewMode", 0)) 356 y += 26 357 self._ui.viewDirectionLabel = vanilla.TextBox( 358 (margin, y + 4, labelW, 14), "Direction", sizeStyle="small" 359 ) 360 self._ui.viewDirection = vanilla.SegmentedButton( 361 (margin + labelW, y, -margin, 18), 362 [dict(title="Rows"), dict(title="Columns")], 363 callback=self._onViewDirectionChanged, 364 sizeStyle="small", 365 ) 366 self._ui.viewDirection.set(self._state.get("viewDirection", 0)) 367 self._refreshViewModeControls() 368 y += 30 369 self._ui.fontList = vanilla.List( 370 (margin, y, -margin, -margin), 371 [], 372 columnDescriptions=[ 373 { 374 "title": "Selected", 375 "cell": vanilla.CheckBoxListCell(), 376 "key": "Selected", 377 "editable": True, 378 "width": 56, 379 }, 380 {"title": "Font Name", "key": "Font Name", "editable": False}, 381 ], 382 showColumnTitles=True, 383 allowsMultipleSelection=True, 384 editCallback=self._onFontListEdited, 385 ) 386 387 # Prompt on startup if no valid folder is configured. 388 if not os.path.isdir(self._state.get("fontFolder", "")): 389 self._promptForFontFolder(triggerChange=False) 390 self._refreshFontRows() 391 392 parent.body = self._ui
Build panel UI in the provided parent group.
def
getPerforationParams(self, pageIndex: int) -> dict | None:
968 def getPerforationParams(self, pageIndex: int) -> dict | None: 969 """Return perforation params for a 0-based page index, or None if disabled/skipped. 970 971 Perforations are drawn only on odd pages (1, 3, 5 … in 1-based terms, 972 i.e. page indices 0, 2, 4 … in 0-based terms). The returned dict can 973 be unpacked directly into ``printing.drawPerforationCircles``: 974 975 params = panel.getPerforationParams(pageIndex) 976 if params: 977 printing.drawPerforationCircles(container, **params) 978 """ 979 if not self._state.get("perforation", False): 980 return None 981 if pageIndex % 2 != 0: 982 return None 983 return { 984 "radius": self._state.get("perforationRadius", 2.0), 985 "spacing": self._state.get("perforationSpacing", 80.0), 986 "count": self._state.get("perforationCount", 2), 987 "align": ( 988 self._state.get("perforationAlignX", "left"), 989 self._state.get("perforationAlignY", "center"), 990 ), 991 }
Return perforation params for a 0-based page index, or None if disabled/skipped.
Perforations are drawn only on odd pages (1, 3, 5 … in 1-based terms,
i.e. page indices 0, 2, 4 … in 0-based terms). The returned dict can
be unpacked directly into printing.drawPerforationCircles:
params = panel.getPerforationParams(pageIndex)
if params:
printing.drawPerforationCircles(container, **params)