Skip to content

Commit fab4a87

Browse files
committed
fix: queue paused by default + manage templates moved to addon prefs
Three usability fixes from user feedback: 1. Render queue now defaults to PAUSED. Spawning a Blender subprocess is a heavyweight side-effect (CPU/GPU, writes files); the user should opt in by toggling Auto-Render: ON rather than have jobs fire automatically the moment they're queued. 2. Pause toggle wording rewritten to describe state + action. Was "Auto-Process" / "Paused" — ambiguous about whether the button shows the current state or the action it would take. Now reads "Auto-Render: ON (queue runs automatically)" or "Auto-Render: OFF (queue is paused)" so it's self-explanatory. 3. Manage Templates moved out of the templates dropdown menu into addon preferences (Edit > Preferences > Add-ons > Stage). Popup dialogs invoked from menu items race with menu dismissal in Blender, so the previous invoke_props_dialog never reliably appeared — clicking the menu entry did nothing visible. The new approach: clicking Manage Templates… opens addon prefs and focuses Stage; user templates list is rendered inline there with delete buttons per row. Always accessible, no popup race. Refactor: introduces queue_ops._current_blend_is_dirty() indirection so the queue's "save first" check can be monkey-patched from tests without touching bpy.data.is_dirty (which is read-only and impossible to clear in a shared session). Total suite: 170/171 (one pre-existing dirty-state reload flake).
1 parent 2bce9cf commit fab4a87

5 files changed

Lines changed: 83 additions & 39 deletions

File tree

stage/ops/queue.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ def _current_blend_filepath() -> str:
3131
return bpy.data.filepath
3232

3333

34+
def _current_blend_is_dirty() -> bool:
35+
"""Indirection over bpy.data.is_dirty so tests can monkeypatch the
36+
'has unsaved changes' check (bpy.data.is_dirty is read-only and
37+
practically impossible to clear in a shared test session)."""
38+
return bpy.data.is_dirty
39+
40+
3441
def _blend_path_or_warn(self, context) -> str | None:
3542
"""Return the absolute path of the saved .blend, or report a warning
3643
and return None if the file is unsaved or has dirty in-memory edits.
@@ -49,7 +56,7 @@ def _blend_path_or_warn(self, context) -> str | None:
4956
"subprocess loads from disk, so unsaved scenes can't be used.",
5057
)
5158
return None
52-
if bpy.data.is_dirty:
59+
if _current_blend_is_dirty():
5360
self.report(
5461
{'ERROR'},
5562
"Your .blend has unsaved changes. Save first (Ctrl+S) — the "

stage/ops/templates.py

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -193,39 +193,29 @@ def execute(self, context):
193193

194194
class STAGE_OT_manage_templates(Operator):
195195
bl_idname = "stage.manage_templates"
196-
bl_label = "Manage User Templates"
197-
bl_description = "Browse and delete user-saved render-settings templates"
196+
bl_label = "Manage User Templates…"
197+
bl_description = (
198+
"Open Edit > Preferences > Add-ons > Stage — user templates are "
199+
"editable inline there with delete buttons per row"
200+
)
198201
bl_options = {'REGISTER'}
199202

200-
def invoke(self, context, event):
201-
return context.window_manager.invoke_props_dialog(self, width=420)
202-
203-
def draw(self, context):
204-
layout = self.layout
205-
prefs = get_prefs(context)
206-
if prefs is None:
207-
layout.label(text="Addon preferences unavailable.", icon='ERROR')
208-
return
209-
210-
if not len(prefs.user_templates):
211-
layout.label(
212-
text="No user templates yet. Use 'Save Current as Template…' to create one.",
213-
icon='INFO',
214-
)
215-
return
216-
217-
layout.label(text=f"{len(prefs.user_templates)} user template(s):")
218-
for i, ut in enumerate(prefs.user_templates):
219-
row = layout.row(align=True)
220-
row.label(
221-
text=f"{ut.display_name} [{ut.engine} {ut.resolution_x}×{ut.resolution_y}]",
222-
icon='PRESET',
223-
)
224-
op = row.operator("stage.delete_user_template", icon='X', text="")
225-
op.index = i
226-
227203
def execute(self, context):
228-
# Popup-only operator. No execute work — closing the popup is enough.
204+
# Popup dialogs invoked from menu items race with menu dismissal
205+
# in Blender, so popups never reliably appear. Sidestep the
206+
# whole issue: open the addon-prefs window and scroll to Stage.
207+
# The user templates list lives in StagePreferences.draw().
208+
try:
209+
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
210+
context.preferences.active_section = 'ADDONS'
211+
context.window_manager.addon_search = "Stage"
212+
except Exception as e:
213+
self.report({'WARNING'}, f"Couldn't open preferences: {e}")
214+
return {'CANCELLED'}
215+
self.report(
216+
{'INFO'},
217+
"Edit user templates inline in Edit › Preferences › Add-ons › Stage",
218+
)
229219
return {'FINISHED'}
230220

231221

stage/prefs.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,20 @@ class StagePreferences(AddonPreferences):
9292
# forward across versions.
9393
user_templates: CollectionProperty(type=UserTemplate)
9494

95-
# Render queue — when paused, the monitor timer doesn't auto-spawn the
96-
# next worker even if PENDING jobs exist. User has to hit "Start Worker"
97-
# to kick a single job, or untick Pause to resume auto-processing.
95+
# Render queue — when paused (the default), the monitor timer doesn't
96+
# auto-spawn workers even if PENDING jobs exist. User has to hit
97+
# "Start Worker" to kick a single job, or flip the toggle to enable
98+
# automatic processing. Paused-by-default because spawning a Blender
99+
# subprocess is a heavyweight side-effect (renders eat CPU/GPU and
100+
# write files) that the user should opt into explicitly.
98101
queue_paused: BoolProperty(
99102
name="Pause Render Queue",
100103
description=(
101-
"Don't auto-start workers when jobs are queued. Use Start Worker "
102-
"to process one job manually, or untick to resume auto-processing"
104+
"When ON: queue accepts jobs but doesn't auto-start workers. "
105+
"Use Start Worker to process one job at a time. "
106+
"When OFF: queue automatically starts the next pending job"
103107
),
104-
default=False,
108+
default=True,
105109
)
106110

107111
# Logging
@@ -130,13 +134,42 @@ def draw(self, context):
130134
col.prop(self, "free_persistent_data_after_render")
131135
col.prop(self, "viewport_solid_during_render")
132136

137+
col = layout.column(align=True)
138+
col.label(text="Render Queue", icon='SCRIPTPLUGINS')
139+
col.prop(self, "queue_paused")
140+
133141
col = layout.column(align=True)
134142
col.label(text="UI", icon='WINDOW')
135143
col.prop(self, "show_dirty_badge")
136144
col.prop(self, "auto_apply_on_select")
137145
col.prop(self, "enable_pie_menu")
138146
col.prop(self, "enable_studio_hotkeys")
139147

148+
# User templates — inline editable list. Lives here rather than in
149+
# a popup because invoke_props_dialog from a menu item races with
150+
# menu dismissal in Blender; addon prefs is always accessible and
151+
# gives the user real management space.
152+
box = layout.box()
153+
header = box.row(align=True)
154+
header.label(
155+
text=f"User Render Templates ({len(self.user_templates)})",
156+
icon='PRESET',
157+
)
158+
if not len(self.user_templates):
159+
box.label(
160+
text="None yet. Use 'Save Current as Template…' from the Stage panel templates menu.",
161+
icon='INFO',
162+
)
163+
else:
164+
for i, ut in enumerate(self.user_templates):
165+
row = box.row(align=True)
166+
row.label(
167+
text=f"{ut.display_name}{ut.engine} {ut.resolution_x}×{ut.resolution_y}",
168+
icon='PRESET_NEW',
169+
)
170+
op = row.operator("stage.delete_user_template", icon='X', text="")
171+
op.index = i
172+
140173
col = layout.column(align=True)
141174
col.label(text="Diagnostics", icon='CONSOLE')
142175
col.prop(self, "log_level")

stage/ui/queue_panel.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import bpy
1818
from bpy.types import Panel
1919

20+
from ..ops import queue as queue_ops
2021
from ..prefs import get_prefs
2122
from ..queue import db as queue_db
2223
from ..queue import worker as queue_worker
@@ -75,7 +76,7 @@ def draw(self, context):
7576
# Studio (output path, captured facets, etc.) won't propagate
7677
# until the user saves. Surface this prominently so they don't
7778
# hit a queue with stale data.
78-
if bpy.data.is_dirty:
79+
if queue_ops._current_blend_is_dirty():
7980
warn = layout.box()
8081
warn_row = warn.row()
8182
warn_row.alert = True
@@ -98,13 +99,19 @@ def draw(self, context):
9899

99100
# Pause toggle drives auto-spawn behaviour in queue.monitor.
100101
# When paused the user must hit Start Worker to process one job.
102+
# Label describes the CURRENT state plus the action that toggle
103+
# would take, so the button is always self-explanatory.
101104
prefs = get_prefs(context)
102105
if prefs is not None:
103106
paused = prefs.queue_paused
104107
row = col.row(align=True)
105108
row.prop(
106109
prefs, "queue_paused",
107-
text="Paused" if paused else "Auto-Process",
110+
text=(
111+
"Auto-Render: OFF (queue is paused)"
112+
if paused
113+
else "Auto-Render: ON (queue runs automatically)"
114+
),
108115
icon='PAUSE' if paused else 'PLAY',
109116
toggle=True,
110117
)

tests/blender/test_queue.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
_TEST_DB_DIR: Path | None = None
3232
_REAL_QUEUE_DB_PATH = queue_paths.queue_db_path
3333
_REAL_BLEND_FILEPATH_FN = queue_ops._current_blend_filepath
34+
_REAL_BLEND_DIRTY_FN = queue_ops._current_blend_is_dirty
3435

3536

3637
def _redirect_db_to_temp() -> Path:
@@ -54,11 +55,17 @@ def _restore_real_db_path() -> None:
5455

5556

5657
def _stub_blend_path(value: str) -> None:
58+
"""Pretend the .blend lives at ``value`` AND is clean (saved).
59+
Tests just want to exercise the operator logic; the in-memory dirty
60+
state is a Blender-session quirk that doesn't reflect the test scene.
61+
"""
5762
queue_ops._current_blend_filepath = lambda: value # type: ignore[assignment]
63+
queue_ops._current_blend_is_dirty = lambda: False # type: ignore[assignment]
5864

5965

6066
def _restore_blend_path() -> None:
6167
queue_ops._current_blend_filepath = _REAL_BLEND_FILEPATH_FN # type: ignore[assignment]
68+
queue_ops._current_blend_is_dirty = _REAL_BLEND_DIRTY_FN # type: ignore[assignment]
6269

6370

6471
def _fresh_scene(name: str = "stage_queue_test"):

0 commit comments

Comments
 (0)