Coverage for paintmaskanimator/preferences_dialog.py: 81%

503 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 04:50 +0000

1"""Preferences window (環境設定). 

2 

3Every control applies and saves its value as soon as it changes, like a macOS 

4settings window, so there is no OK / Cancel. The window is not modal and is 

5kept on the main window, so reopening it (⌘, / Ctrl+,) raises the same one. 

6Keyboard shortcuts stay in their own dialog: they are a long list of their own 

7and would crowd out everything else here. 

8""" 

9from pathlib import Path 

10 

11from PySide6.QtCore import QSize, Qt, Signal 

12from PySide6.QtGui import QColor, QIcon, QPixmap 

13from PySide6.QtWidgets import ( 

14 QAbstractItemView, 

15 QGridLayout, 

16 QCheckBox, 

17 QComboBox, 

18 QDialog, 

19 QDialogButtonBox, 

20 QFileDialog, 

21 QFormLayout, 

22 QHBoxLayout, 

23 QInputDialog, 

24 QLabel, 

25 QListWidget, 

26 QListWidgetItem, 

27 QMessageBox, 

28 QPushButton, 

29 QSpinBox, 

30 QStackedWidget, 

31 QVBoxLayout, 

32 QWidget, 

33) 

34 

35from . import (config, display_shrink, i18n, layout_paper, preferences, 

36 pressure_settings, theme, view_preview) 

37from .canvas_undo import set_undo_limits 

38from .i18n import tr 

39from .color_picker_dialog import ColorPickerDialog 

40from .pressure import PressureEditor 

41 

42_CUSTOM_ACCENT = "custom" 

43# Colours come from the app palette so the theme switch restyles them too. 

44_STYLE = """ 

45QLabel#preferencesHint { color: palette(placeholder-text); } 

46""" 

47 

48 

49def _swatch(color): 

50 pixmap = QPixmap(14, 14) 

51 pixmap.fill(QColor(color)) 

52 return QIcon(pixmap) 

53 

54 

55def _hint(text): 

56 label = QLabel(text) 

57 label.setWordWrap(True) 

58 label.setObjectName("preferencesHint") 

59 return label 

60 

61 

62def _page(*rows): 

63 """A form page. ``rows`` are ``(label, widget)`` pairs or lone hint widgets.""" 

64 page = QWidget() 

65 form = QFormLayout(page) 

66 form.setContentsMargins(20, 16, 20, 16) 

67 form.setVerticalSpacing(10) 

68 form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.FieldsStayAtSizeHint) 

69 for row in rows: 

70 if isinstance(row, tuple): 

71 form.addRow(*row) 

72 else: 

73 form.addRow(row) 

74 return page 

75 

76 

77class _ReorderableList(QListWidget): 

78 """A list the user can reorder by dragging; ``orderChanged`` after a drop.""" 

79 

80 orderChanged = Signal() 

81 

82 def __init__(self, parent=None): 

83 super().__init__(parent) 

84 self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) 

85 self.setDefaultDropAction(Qt.DropAction.MoveAction) 

86 

87 def dropEvent(self, event): 

88 super().dropEvent(event) 

89 self.orderChanged.emit() 

90 

91 

92class PreferencesDialog(QDialog): 

93 def __init__(self, window): 

94 super().__init__(window) 

95 self.main_window = window 

96 self.setWindowTitle(tr("環境設定")) 

97 self.setModal(False) 

98 self.resize(620, 520) 

99 

100 self.setStyleSheet(_STYLE) 

101 self.sections = QListWidget() 

102 self.sections.setObjectName("preferencesSections") 

103 self.sections.setFixedWidth(160) 

104 self.pages = QStackedWidget() 

105 self._section_keys = [] 

106 for key, title, page in ( 

107 ("general", tr("一般"), self._build_general_page()), 

108 ("appearance", tr("外観"), self._build_appearance_page()), 

109 ("canvas", tr("新規キャンバス"), self._build_canvas_page()), 

110 ("pressure", tr("筆圧"), self._build_pressure_page()), 

111 ("view_check", tr("表示チェック"), self._build_view_check_page()), 

112 ("workspaces", tr("ワークスペース"), self._build_workspaces_page()), 

113 ("performance", tr("パフォーマンス"), self._build_performance_page()), 

114 ): 

115 item = QListWidgetItem(title) 

116 item.setSizeHint(QSize(0, 30)) 

117 self.sections.addItem(item) 

118 self.pages.addWidget(page) 

119 self._section_keys.append(key) 

120 self.sections.currentRowChanged.connect(self.pages.setCurrentIndex) 

121 self.sections.setCurrentRow(0) 

122 

123 body = QHBoxLayout() 

124 body.setContentsMargins(0, 0, 0, 0) 

125 body.addWidget(self.sections) 

126 body.addWidget(self.pages, 1) 

127 layout = QVBoxLayout(self) 

128 layout.addLayout(body, 1) 

129 # macOS settings windows have no buttons; elsewhere people look for one. 

130 if not theme.IS_MAC: 130 ↛ exitline 130 didn't return from function '__init__' because the condition on line 130 was always true

131 buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) 

132 buttons.rejected.connect(self.close) 

133 layout.addWidget(buttons) 

134 

135 # -- 一般 --------------------------------------------------------------- 

136 def _build_general_page(self): 

137 self.language = QComboBox() 

138 self.language.addItem(tr("システムに合わせる"), i18n.SYSTEM) 

139 for code, label in i18n.available_languages().items(): 

140 self.language.addItem(label, code) 

141 self._select_data(self.language, i18n.preferred_language()) 

142 self.language.currentIndexChanged.connect(self._language_changed) 

143 

144 self.autosave_enabled = QCheckBox(tr("自動保存する")) 

145 self.autosave_enabled.setChecked(preferences.autosave_enabled()) 

146 self.autosave_enabled.toggled.connect(self._autosave_changed) 

147 self.autosave_interval = QSpinBox() 

148 self.autosave_interval.setRange(*preferences.AUTOSAVE_INTERVAL_RANGE) 

149 self.autosave_interval.setSuffix(tr(" 分ごと")) 

150 self.autosave_interval.setValue(preferences.autosave_interval_minutes()) 

151 self.autosave_interval.setEnabled(self.autosave_enabled.isChecked()) 

152 self.autosave_interval.valueChanged.connect(self._autosave_changed) 

153 

154 self.keep_initial_state = QCheckBox( 

155 tr("カットフォルダーの読み込みは常に色塗り前の初期状態として保存する") 

156 ) 

157 self.keep_initial_state.setChecked(preferences.keep_initial_state_on_cut_folder()) 

158 self.keep_initial_state.toggled.connect( 

159 preferences.set_keep_initial_state_on_cut_folder 

160 ) 

161 

162 self.check_updates = QCheckBox(tr("起動したら新しいバージョンを確かめる")) 

163 self.check_updates.setChecked(preferences.check_updates_automatically()) 

164 self.check_updates.toggled.connect(preferences.set_check_updates_automatically) 

165 

166 self.tool_hold_enabled = QCheckBox( 

167 tr("ツールのキーを長押ししたら、離したときに元のツールへ戻す") 

168 ) 

169 self.tool_hold_enabled.setChecked(preferences.tool_hold_enabled()) 

170 self.tool_hold_enabled.toggled.connect(self._tool_hold_changed) 

171 self.tool_hold_ms = QSpinBox() 

172 self.tool_hold_ms.setRange(*preferences.TOOL_HOLD_MS_RANGE) 

173 self.tool_hold_ms.setSingleStep(50) 

174 self.tool_hold_ms.setSuffix(tr(" ms 以上押したら")) 

175 self.tool_hold_ms.setValue(preferences.tool_hold_ms()) 

176 self.tool_hold_ms.setEnabled(self.tool_hold_enabled.isChecked()) 

177 self.tool_hold_ms.valueChanged.connect(self._tool_hold_changed) 

178 

179 self.background_color_tool = QComboBox() 

180 self.background_color_tool.addItem(tr("ブラシ"), "brush") 

181 self.background_color_tool.addItem(tr("バケツ"), "bucket") 

182 self._select_data(self.background_color_tool, preferences.background_color_tool()) 

183 self.background_color_tool.currentIndexChanged.connect( 

184 lambda: preferences.set_background_color_tool(self.background_color_tool.currentData()) 

185 ) 

186 

187 self.time_display = QComboBox() 

188 self.time_display.addItem(tr("1コマ目"), preferences.TIME_DISPLAY_FRAMES) 

189 self.time_display.addItem("0+1", preferences.TIME_DISPLAY_SECONDS_FRAMES) 

190 self._select_data(self.time_display, preferences.time_display_mode()) 

191 self.time_display.currentIndexChanged.connect(self._time_display_changed) 

192 

193 return _page( 

194 (tr("言語 / Language"), self.language), 

195 _hint(tr("言語は次回の起動から切り替わります。")), 

196 (tr("時間の表示"), self.time_display), 

197 (tr("背景色選択時の描画ツール"), self.background_color_tool), 

198 _hint(tr("描画ツール以外で背景色のショートカット(E)を押したときに使います。")), 

199 (tr("初期状態"), self.keep_initial_state), 

200 _hint(tr("初期状態として保存すると自動色塗りの精度が上がります。外すと、読み込み" 

201 "のたびに保存するか尋ねます。")), 

202 (tr("自動保存"), self.autosave_enabled), 

203 ("", self.autosave_interval), 

204 _hint(tr("アプリが異常終了したとき、次の起動で自動保存から作業を復元できます。")), 

205 (tr("更新"), self.check_updates), 

206 _hint(tr( 

207 "新しいバージョンがあると、メニューバーの右端(macOS では画面上部の" 

208 "メニューの最後)でお知らせします。ヘルプ › 更新を確認… はいつでも使えます。" 

209 )), 

210 (tr("ツールの一時切り替え"), self.tool_hold_enabled), 

211 ("", self.tool_hold_ms), 

212 _hint(tr( 

213 "ツールのキー(P・G など)を押すとすぐ切り替わり、この長さ以上押してから" 

214 "離すと元のツールへ戻ります。0 ms にすると押している間だけ切り替わり、" 

215 "離せば必ず元のツールに戻ります(完全な一時切り替え)。短く押して" 

216 "切り替えたままにしたいときは 300〜500 ms ほどにします。" 

217 )), 

218 ) 

219 

220 def _time_display_changed(self): 

221 preferences.set_time_display_mode(self.time_display.currentData()) 

222 self.main_window.refresh_ui() 

223 

224 def _tool_hold_changed(self): 

225 preferences.set_tool_hold_enabled(self.tool_hold_enabled.isChecked()) 

226 preferences.set_tool_hold_ms(self.tool_hold_ms.value()) 

227 self.tool_hold_ms.setEnabled(self.tool_hold_enabled.isChecked()) 

228 

229 def _language_changed(self): 

230 i18n.set_preferred_language(self.language.currentData()) 

231 

232 def _autosave_changed(self): 

233 enabled = self.autosave_enabled.isChecked() 

234 self.autosave_interval.setEnabled(enabled) 

235 preferences.set_autosave_enabled(enabled) 

236 preferences.set_autosave_interval_minutes(self.autosave_interval.value()) 

237 self.main_window.autosave.apply_preferences() 

238 

239 # -- 外観 --------------------------------------------------------------- 

240 def _build_appearance_page(self): 

241 self.theme = QComboBox() 

242 labels = { 

243 theme.SYSTEM: tr("システムに合わせる"), 

244 "light": tr("ライト(明るい)"), 

245 "dark": tr("ダーク(暗い)"), 

246 } 

247 for name in theme.available_themes(): 

248 self.theme.addItem(labels.get(name, name), name) 

249 self._select_data(self.theme, theme.current_theme()) 

250 self.theme.currentIndexChanged.connect(self._theme_changed) 

251 

252 self.accent = QComboBox() 

253 self._fill_accent_choices() 

254 self.accent.activated.connect(self._accent_chosen) 

255 

256 self.background_checker = QCheckBox(tr("指定した背景色を市松模様で表示する")) 

257 self.background_checker.setChecked(preferences.background_checker_enabled()) 

258 self.background_checker.toggled.connect(self._background_checker_changed) 

259 self.background_checker_color = QPushButton() 

260 self._refresh_background_checker_color() 

261 self.background_checker_color.clicked.connect(self._choose_background_checker_color) 

262 

263 return _page( 

264 (tr("テーマ"), self.theme), 

265 (tr("アクセントカラー"), self.accent), 

266 (tr("背景の表示"), self.background_checker), 

267 (tr("市松模様にする背景色"), self.background_checker_color), 

268 _hint(tr("背景の表示色が指定色と一致するときに適用します。画像や書き出しには影響しません。")), 

269 ) 

270 

271 def _refresh_background_checker_color(self): 

272 color = QColor(*preferences.background_checker_color()) 

273 self.background_checker_color.setText(color.name().upper()) 

274 self.background_checker_color.setIcon(_swatch(color)) 

275 

276 def _background_checker_changed(self, enabled): 

277 preferences.set_background_checker_enabled(enabled) 

278 self.main_window.canvas.update() 

279 

280 def _choose_background_checker_color(self): 

281 color = ColorPickerDialog.get_color( 

282 QColor(*preferences.background_checker_color()), self, 

283 tr("市松模様にする背景色を選択"), 

284 ) 

285 if color.isValid(): 

286 preferences.set_background_checker_color(color.name()) 

287 self._refresh_background_checker_color() 

288 self.main_window.canvas.update() 

289 

290 def _theme_changed(self): 

291 self.main_window.set_theme(self.theme.currentData()) 

292 

293 def _fill_accent_choices(self): 

294 self.accent.blockSignals(True) 

295 self.accent.clear() 

296 self.accent.addItem(tr("システムに合わせる"), theme.SYSTEM) 

297 presets = theme.accent_presets() 

298 for label, hexval in presets: 

299 self.accent.addItem(_swatch(hexval), label, hexval) 

300 current = theme.accent_setting() 

301 if current != theme.SYSTEM and current not in {h for _, h in presets}: 

302 self.accent.addItem(_swatch(current), tr("カスタム({color})").format(color=current), current) 

303 self.accent.addItem(tr("カスタム…"), _CUSTOM_ACCENT) 

304 self._select_data(self.accent, current) 

305 self.accent.blockSignals(False) 

306 

307 def _accent_chosen(self): 

308 value = self.accent.currentData() 

309 if value == _CUSTOM_ACCENT: 309 ↛ 310line 309 didn't jump to line 310 because the condition on line 309 was never true

310 color = ColorPickerDialog.get_color( 

311 QColor(theme.current_accent()), self, tr("アクセントカラーを選択") 

312 ) 

313 if not color.isValid(): 

314 self._fill_accent_choices() 

315 return 

316 value = color.name() 

317 self.main_window.colors.set_accent(value) 

318 self._fill_accent_choices() 

319 

320 # -- 表示チェック --------------------------------------------------------- 

321 def _build_view_check_page(self): 

322 grid = QGridLayout() 

323 grid.setContentsMargins(0, 0, 0, 0) 

324 grid.setSpacing(6) 

325 self.map_palette_buttons = [] 

326 columns = 6 

327 for index in range(len(view_preview.MAP_PALETTE)): 

328 button = QPushButton() 

329 button.setFixedSize(40, 28) 

330 button.setToolTip(tr("{number} 番目の色(クリックで変更)").format(number=index + 1)) 

331 button.clicked.connect(lambda _checked=False, i=index: self._choose_map_color(i)) 

332 grid.addWidget(button, index // columns, index % columns) 

333 self.map_palette_buttons.append(button) 

334 palette_box = QWidget() 

335 palette_box.setLayout(grid) 

336 reset = QPushButton(tr("既定に戻す")) 

337 reset.clicked.connect(self._reset_map_palette) 

338 reset_row = QHBoxLayout() 

339 reset_row.setContentsMargins(0, 0, 0, 0) 

340 reset_row.addWidget(reset) 

341 reset_row.addStretch() 

342 reset_box = QWidget() 

343 reset_box.setLayout(reset_row) 

344 self._show_map_palette() 

345 return _page( 

346 (tr("隣接色分けの色"), palette_box), 

347 ("", reset_box), 

348 _hint(tr( 

349 "キャンバスの下の「隣接色分け」で、隣り合う部分を塗り分ける仮の色です。" 

350 "大きい部分から順に、隣でまだ使っていない色を 1 番から選ぶので、" 

351 "前の番号の色ほど多く使われます。見分けやすいよう、互いに離れた色に" 

352 "してください。主線(黒)と背景(白)に近い色は避けると見やすくなります。" 

353 )), 

354 ) 

355 

356 def _show_map_palette(self): 

357 for button, rgb in zip(self.map_palette_buttons, preferences.map_palette()): 

358 color = QColor(*rgb) 

359 button.setStyleSheet( 

360 "QPushButton {{ background-color: {0}; border: 1px solid palette(mid);" 

361 " border-radius: 4px; }}".format(color.name()) 

362 ) 

363 button.setAccessibleName(color.name()) 

364 

365 def _choose_map_color(self, index): 

366 colors = list(preferences.map_palette()) 

367 color = ColorPickerDialog.get_color( 

368 QColor(*colors[index]), self, tr("隣接色分けの色を選択") 

369 ) 

370 if not color.isValid(): 370 ↛ 371line 370 didn't jump to line 371 because the condition on line 370 was never true

371 return 

372 colors[index] = (color.red(), color.green(), color.blue()) 

373 self.main_window.view_preview.set_palette(colors) 

374 self._show_map_palette() 

375 

376 def _reset_map_palette(self): 

377 self.main_window.view_preview.set_palette(view_preview.MAP_PALETTE) 

378 self._show_map_palette() 

379 

380 # -- 新規キャンバス ------------------------------------------------------- 

381 def _build_canvas_page(self): 

382 width, height = preferences.new_canvas_size() 

383 self.canvas_width = self._size_spin(width) 

384 self.canvas_height = self._size_spin(height) 

385 

386 self.papers = QListWidget() 

387 self.papers.setIconSize(QSize(40, 40)) 

388 self.papers.setMinimumHeight(150) 

389 add = QPushButton(tr("追加…")) 

390 add.clicked.connect(self._add_papers) 

391 self.rename_paper_button = QPushButton(tr("名前を変更…")) 

392 self.rename_paper_button.clicked.connect(self._rename_paper) 

393 self.remove_paper_button = QPushButton(tr("削除")) 

394 self.remove_paper_button.clicked.connect(self._remove_paper) 

395 buttons = QHBoxLayout() 

396 buttons.setContentsMargins(0, 0, 0, 0) 

397 for button in (add, self.rename_paper_button, self.remove_paper_button): 

398 buttons.addWidget(button) 

399 buttons.addStretch() 

400 paper_box = QWidget() 

401 paper_layout = QVBoxLayout(paper_box) 

402 paper_layout.setContentsMargins(0, 0, 0, 0) 

403 paper_layout.addWidget(self.papers, 1) 

404 paper_layout.addLayout(buttons) 

405 self.papers.currentRowChanged.connect(self._sync_paper_buttons) 

406 self.reload_papers() 

407 

408 page = _page( 

409 (tr("幅"), self.canvas_width), 

410 (tr("高さ"), self.canvas_height), 

411 _hint(tr("起動したときと、ファイル › 新規作成… で使う大きさです。")), 

412 (tr("レイアウト用紙"), paper_box), 

413 _hint(tr( 

414 "ファイル › 新規作成… で選ぶと、一番下の下書きレイヤーとして入り、" 

415 "キャンバスサイズを用紙に合わせられます。画像は環境設定の中に" 

416 "コピーして保管します。" 

417 )), 

418 ) 

419 form = page.layout() 

420 assert isinstance(form, QFormLayout) 

421 form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) 

422 return page 

423 

424 def _size_spin(self, value): 

425 spin = QSpinBox() 

426 spin.setRange(*preferences.CANVAS_SIZE_RANGE) 

427 spin.setSuffix(" px") 

428 spin.setValue(value) 

429 spin.valueChanged.connect(self._canvas_size_changed) 

430 return spin 

431 

432 def _canvas_size_changed(self): 

433 preferences.set_new_canvas_size( 

434 self.canvas_width.value(), self.canvas_height.value() 

435 ) 

436 

437 def reload_papers(self, select_id=None): 

438 self.papers.clear() 

439 for entry in layout_paper.papers(): 439 ↛ 440line 439 didn't jump to line 440 because the loop on line 439 never started

440 size = layout_paper.image_size(entry) 

441 label = entry["name"] if size is None else f"{entry['name']}\n{size[0]} × {size[1]} px" 

442 item = QListWidgetItem(label) 

443 item.setData(Qt.ItemDataRole.UserRole, entry["id"]) 

444 thumbnail = QPixmap(str(layout_paper.image_path(entry))) 

445 if not thumbnail.isNull(): 

446 item.setIcon(QIcon(thumbnail.scaled( 

447 40, 40, 

448 Qt.AspectRatioMode.KeepAspectRatio, 

449 Qt.TransformationMode.SmoothTransformation, 

450 ))) 

451 self.papers.addItem(item) 

452 if entry["id"] == select_id: 

453 self.papers.setCurrentItem(item) 

454 self._sync_paper_buttons() 

455 

456 def _selected_paper_id(self): 

457 item = self.papers.currentItem() 

458 return None if item is None else item.data(Qt.ItemDataRole.UserRole) 

459 

460 def _sync_paper_buttons(self, *_args): 

461 selected = self._selected_paper_id() is not None 

462 self.rename_paper_button.setEnabled(selected) 

463 self.remove_paper_button.setEnabled(selected) 

464 

465 def _add_papers(self): 

466 paths, _ = QFileDialog.getOpenFileNames( 

467 self, 

468 tr("レイアウト用紙の画像を追加"), 

469 "", 

470 tr("画像 (*.png *.jpg *.jpeg *.tga)"), 

471 ) 

472 added = None 

473 failed = [] 

474 for path in paths: 

475 entry = layout_paper.add_paper(path) 

476 if entry is None: 

477 failed.append(Path(path).name) 

478 else: 

479 added = entry 

480 self.reload_papers(select_id=added["id"] if added else None) 

481 if failed: 

482 QMessageBox.warning( 

483 self, 

484 tr("レイアウト用紙"), 

485 tr("次の画像は読み込めませんでした。\n\n{names}").format(names="\n".join(failed)), 

486 ) 

487 

488 def _rename_paper(self): 

489 paper_id = self._selected_paper_id() 

490 entry = layout_paper.paper(paper_id) 

491 if entry is None: 

492 return 

493 name, ok = QInputDialog.getText( 

494 self, tr("名前を変更"), tr("用紙の名前"), text=entry["name"] 

495 ) 

496 if ok and name.strip(): 

497 layout_paper.rename_paper(paper_id, name) 

498 self.reload_papers(select_id=paper_id) 

499 

500 def _remove_paper(self): 

501 paper_id = self._selected_paper_id() 

502 if paper_id is not None: 

503 layout_paper.remove_paper(paper_id) 

504 self.reload_papers() 

505 

506 # -- 筆圧 --------------------------------------------------------------- 

507 def _build_pressure_page(self): 

508 self.pressure_preset = QComboBox() 

509 self.pressure_preset.currentIndexChanged.connect(self._pressure_preset_chosen) 

510 add = QPushButton(tr("追加")) 

511 add.setToolTip(tr("今のプリセットを元に、新しいプリセットを作ります。")) 

512 add.clicked.connect(self._add_pressure_preset) 

513 rename = QPushButton(tr("名前を変更…")) 

514 rename.clicked.connect(self._rename_pressure_preset) 

515 self.remove_pressure_button = QPushButton(tr("削除")) 

516 self.remove_pressure_button.clicked.connect(self._remove_pressure_preset) 

517 preset_row = QHBoxLayout() 

518 preset_row.setContentsMargins(0, 0, 0, 0) 

519 preset_row.addWidget(self.pressure_preset, 1) 

520 for button in (add, rename, self.remove_pressure_button): 

521 preset_row.addWidget(button) 

522 

523 self.pressure_editor = PressureEditor() 

524 self.pressure_editor.changed.connect(self._pressure_edited) 

525 self.reload_pressure_presets() 

526 

527 page = QWidget() 

528 layout = QVBoxLayout(page) 

529 layout.setContentsMargins(20, 16, 20, 16) 

530 layout.setSpacing(10) 

531 layout.addWidget(QLabel(tr("使うプリセット"))) 

532 layout.addLayout(preset_row) 

533 layout.addWidget(_hint(tr( 

534 "ブラシが「全体の設定を使う」のとき、ここで選んだプリセットで描きます。" 

535 "ブラシだけの設定は、ツールプロパティの 筆圧… で切り替えます。" 

536 ))) 

537 layout.addWidget(self.pressure_editor, 1) 

538 return page 

539 

540 def reload_pressure_presets(self): 

541 self.pressure_preset.blockSignals(True) 

542 self.pressure_preset.clear() 

543 self.pressure_preset.addItems(pressure_settings.preset_names()) 

544 self.pressure_preset.setCurrentText(pressure_settings.active_preset_name()) 

545 self.pressure_preset.blockSignals(False) 

546 self.remove_pressure_button.setEnabled(self.pressure_preset.count() > 1) 

547 self.pressure_editor.set_settings(pressure_settings.global_settings()) 

548 

549 def _pressure_preset_chosen(self): 

550 pressure_settings.set_active_preset(self.pressure_preset.currentText()) 

551 self.pressure_editor.set_settings(pressure_settings.global_settings()) 

552 self.main_window.apply_pressure_settings() 

553 

554 def _pressure_edited(self): 

555 pressure_settings.update_preset( 

556 self.pressure_preset.currentText(), self.pressure_editor.settings() 

557 ) 

558 self.main_window.apply_pressure_settings() 

559 

560 def _add_pressure_preset(self): 

561 name = pressure_settings.add_preset( 

562 tr("新しいプリセット"), self.pressure_editor.settings() 

563 ) 

564 pressure_settings.set_active_preset(name) 

565 self.reload_pressure_presets() 

566 self.main_window.apply_pressure_settings() 

567 

568 def _rename_pressure_preset(self): 

569 old = self.pressure_preset.currentText() 

570 name, ok = QInputDialog.getText( 

571 self, tr("名前を変更"), tr("プリセットの名前"), text=old 

572 ) 

573 if not ok or name.strip() == old: 

574 return 

575 if not pressure_settings.rename_preset(old, name): 

576 QMessageBox.warning( 

577 self, 

578 tr("名前を変更"), 

579 tr("その名前は使えません。空欄か、ほかのプリセットと同じ名前です。"), 

580 ) 

581 return 

582 self.reload_pressure_presets() 

583 

584 def _remove_pressure_preset(self): 

585 if pressure_settings.remove_preset(self.pressure_preset.currentText()): 

586 self.reload_pressure_presets() 

587 self.main_window.apply_pressure_settings() 

588 

589 # -- ワークスペース ----------------------------------------------------- 

590 def _build_workspaces_page(self): 

591 self.workspaces = _ReorderableList() 

592 self.workspaces.orderChanged.connect(self._workspaces_dragged) 

593 self.workspaces.currentRowChanged.connect(self._sync_workspace_buttons) 

594 self.workspaces.itemDoubleClicked.connect(lambda _item: self._rename_workspace()) 

595 

596 self.workspace_up = QPushButton(tr("上へ")) 

597 self.workspace_up.clicked.connect(lambda: self._move_workspace(-1)) 

598 self.workspace_down = QPushButton(tr("下へ")) 

599 self.workspace_down.clicked.connect(lambda: self._move_workspace(1)) 

600 self.workspace_rename = QPushButton(tr("名前を変更…")) 

601 self.workspace_rename.clicked.connect(self._rename_workspace) 

602 self.workspace_overwrite = QPushButton(tr("現在の配置で上書き")) 

603 self.workspace_overwrite.setToolTip( 

604 tr("選んだワークスペースを、今のパネルの配置で保存し直します。") 

605 ) 

606 self.workspace_overwrite.clicked.connect(self._overwrite_workspace) 

607 self.workspace_delete = QPushButton(tr("削除…")) 

608 self.workspace_delete.clicked.connect(self._delete_workspace) 

609 buttons = QVBoxLayout() 

610 buttons.setContentsMargins(0, 0, 0, 0) 

611 for button in ( 

612 self.workspace_up, self.workspace_down, self.workspace_rename, 

613 self.workspace_overwrite, self.workspace_delete, 

614 ): 

615 buttons.addWidget(button) 

616 buttons.addStretch() 

617 

618 page = QWidget() 

619 layout = QVBoxLayout(page) 

620 layout.setContentsMargins(20, 16, 20, 16) 

621 layout.setSpacing(10) 

622 row = QHBoxLayout() 

623 row.addWidget(self.workspaces, 1) 

624 row.addLayout(buttons) 

625 layout.addLayout(row, 1) 

626 layout.addWidget(_hint(tr( 

627 "ワークスペース メニューに、この順で並びます。ドラッグでも並べ替えられます。" 

628 "新しいワークスペースは ワークスペース › 現在の配置を保存… で作ります。" 

629 ))) 

630 self.reload_workspaces() 

631 return page 

632 

633 def reload_workspaces(self): 

634 workspace = self.main_window.workspace 

635 selected = self._selected_workspace() 

636 active = config.get_value("active_workspace") 

637 self.workspaces.blockSignals(True) 

638 self.workspaces.clear() 

639 for name in workspace.names(): 

640 label = tr("{name}(使用中)").format(name=name) if name == active else name 

641 item = QListWidgetItem(label) 

642 item.setData(Qt.ItemDataRole.UserRole, name) 

643 item.setSizeHint(QSize(0, 28)) 

644 self.workspaces.addItem(item) 

645 if name == selected: 

646 self.workspaces.setCurrentItem(item) 

647 self.workspaces.blockSignals(False) 

648 self._sync_workspace_buttons() 

649 

650 def _selected_workspace(self): 

651 item = self.workspaces.currentItem() if hasattr(self, "workspaces") else None 

652 return None if item is None else item.data(Qt.ItemDataRole.UserRole) 

653 

654 def _listed_workspaces(self): 

655 return [ 

656 self.workspaces.item(row).data(Qt.ItemDataRole.UserRole) 

657 for row in range(self.workspaces.count()) 

658 ] 

659 

660 def _sync_workspace_buttons(self, *_args): 

661 row = self.workspaces.currentRow() 

662 selected = row >= 0 

663 self.workspace_up.setEnabled(row > 0) 

664 self.workspace_down.setEnabled(selected and row < self.workspaces.count() - 1) 

665 for button in (self.workspace_rename, self.workspace_overwrite, self.workspace_delete): 

666 button.setEnabled(selected) 

667 

668 def _workspaces_dragged(self, *_args): 

669 self.main_window.workspace.reorder(self._listed_workspaces()) 

670 

671 def _move_workspace(self, step): 

672 row = self.workspaces.currentRow() 

673 names = self._listed_workspaces() 

674 target = row + step 

675 if row < 0 or not 0 <= target < len(names): 675 ↛ 676line 675 didn't jump to line 676 because the condition on line 675 was never true

676 return 

677 names[row], names[target] = names[target], names[row] 

678 self.main_window.workspace.reorder(names) 

679 self.workspaces.setCurrentRow(target) 

680 

681 def _rename_workspace(self): 

682 old = self._selected_workspace() 

683 if old is None: 

684 return 

685 name, ok = QInputDialog.getText( 

686 self, tr("名前を変更"), tr("ワークスペース名:"), text=old 

687 ) 

688 if not ok or name.strip() == old: 688 ↛ 689line 688 didn't jump to line 689 because the condition on line 688 was never true

689 return 

690 if not self.main_window.workspace.rename(old, name): 690 ↛ 691line 690 didn't jump to line 691 because the condition on line 690 was never true

691 QMessageBox.warning( 

692 self, 

693 tr("名前を変更"), 

694 tr("その名前は使えません。空欄か、ほかのワークスペースと同じ名前です。"), 

695 ) 

696 

697 def _overwrite_workspace(self): 

698 name = self._selected_workspace() 

699 if name is not None: 

700 self.main_window.workspace.save(name) 

701 

702 def _delete_workspace(self): 

703 name = self._selected_workspace() 

704 if name is None: 704 ↛ 705line 704 didn't jump to line 705 because the condition on line 704 was never true

705 return 

706 answer = QMessageBox.question( 

707 self, 

708 tr("ワークスペースを削除"), 

709 tr("ワークスペース「{name}」を削除しますか?").format(name=name), 

710 ) 

711 if answer == QMessageBox.StandardButton.Yes: 

712 self.main_window.workspace.delete(name) 

713 

714 # -- パフォーマンス ------------------------------------------------------- 

715 def _build_performance_page(self): 

716 self.display_shrink_mode = QComboBox() 

717 self.display_shrink_mode.addItem( 

718 tr("速さ優先(間引き)"), display_shrink.MODE_NEAREST 

719 ) 

720 self.display_shrink_mode.addItem( 

721 tr("線を残す"), display_shrink.MODE_KEEP_LINES 

722 ) 

723 self._select_data( 

724 self.display_shrink_mode, preferences.display_shrink_mode() 

725 ) 

726 self.display_shrink_mode.currentIndexChanged.connect( 

727 self._display_shrink_mode_changed 

728 ) 

729 self.undo_steps = QSpinBox() 

730 self.undo_steps.setRange(*preferences.UNDO_STEPS_RANGE) 

731 self.undo_steps.setSuffix(tr(" 回")) 

732 self.undo_steps.setValue(preferences.undo_max_steps()) 

733 self.undo_steps.valueChanged.connect(self._undo_limits_changed) 

734 self.undo_memory = QSpinBox() 

735 self.undo_memory.setRange(*preferences.UNDO_MEMORY_RANGE) 

736 self.undo_memory.setSingleStep(128) 

737 self.undo_memory.setSuffix(" MB") 

738 self.undo_memory.setValue(preferences.undo_max_memory_mb()) 

739 self.undo_memory.valueChanged.connect(self._undo_limits_changed) 

740 return _page( 

741 (tr("縮小表示"), self.display_shrink_mode), 

742 _hint(tr( 

743 "キャンバス全体が見えるところまで縮めると、1画素の線は拾われたり" 

744 "落ちたりして荒れます。「線を残す」は細い線を消さずに縮めますが、" 

745 "大きなキャンバスでは表示の更新が少し重くなります。" 

746 "元の絵は変わりません。" 

747 )), 

748 (tr("取り消せる回数"), self.undo_steps), 

749 (tr("取り消し履歴のメモリ"), self.undo_memory), 

750 _hint(tr( 

751 "どちらかの上限に達すると、古い履歴から消えます。" 

752 "大きなキャンバスでは、回数より先にメモリの上限に達します。" 

753 "減らしたときは、次に操作したときに古い履歴が消えます。" 

754 )), 

755 ) 

756 

757 def _display_shrink_mode_changed(self): 

758 preferences.set_display_shrink_mode( 

759 self.display_shrink_mode.currentData() 

760 ) 

761 canvas = getattr(self.main_window, "canvas", None) 

762 if canvas is not None: 

763 canvas.update() 

764 

765 def _undo_limits_changed(self): 

766 preferences.set_undo_max_steps(self.undo_steps.value()) 

767 preferences.set_undo_max_memory_mb(self.undo_memory.value()) 

768 apply_undo_preferences() 

769 

770 # ----------------------------------------------------------------------- 

771 @staticmethod 

772 def _select_data(combo, value): 

773 index = combo.findData(value) 

774 if index >= 0: 774 ↛ exitline 774 didn't return from function '_select_data' because the condition on line 774 was always true

775 combo.setCurrentIndex(index) 

776 

777 def show_section(self, key): 

778 if key in self._section_keys: 778 ↛ exitline 778 didn't return from function 'show_section' because the condition on line 778 was always true

779 self.sections.setCurrentRow(self._section_keys.index(key)) 

780 

781 def show_and_raise(self): 

782 self.show() 

783 self.raise_() 

784 self.activateWindow() 

785 

786 

787def apply_undo_preferences(): 

788 set_undo_limits( 

789 preferences.undo_max_steps(), 

790 preferences.undo_max_memory_mb() * 1024 * 1024, 

791 )