Coverage for graphica/gui/shell/header.py: 99.2%
114 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""上部バー: ≡(メニュー)・ロゴ・プロジェクトのタブ・コマンドを検索・モードの切り替え・元に戻す/やり直す・保存・
2クイックアクセス・エクスポート。高さ 48px の1段。見本は docs/dev/mockups/gui_redesign/。
4押したときの行き先(表示中のタブ)は MainAppWindow がつなぐ。ここは部品と並びだけを持つ。
5"""
6from PySide6.QtCore import QSize, Qt
7from PySide6.QtGui import QIcon
8from PySide6.QtWidgets import (QFrame, QHBoxLayout, QLabel, QPushButton, QSizePolicy, QStackedWidget, QToolButton,
9 QWidget)
11from graphica.core.i18n import tr
12from graphica.gui import ui_mode
13from graphica.gui.shell.icons import themed_icon
14from graphica.gui.widgets import SegmentedControl
16HEADER_HEIGHT = 48
17SEARCH_WIDTH = 300
18COMMAND_PALETTE_SHORTCUT_TEXT = "Ctrl+Shift+P"
21def _header_button(name, tooltip):
22 button = QToolButton()
23 button.setObjectName("header_button")
24 button.setToolTip(tooltip)
25 button.setIconSize(QSize(18, 18))
26 button.setFixedSize(30, 30)
27 button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
28 button.setCursor(Qt.CursorShape.PointingHandCursor)
29 button.setProperty("iconName", name)
30 button.setIcon(themed_icon(name))
31 return button
34def _separator():
35 line = QFrame()
36 line.setObjectName("header_separator")
37 line.setFrameShape(QFrame.Shape.VLine)
38 line.setFixedSize(1, 20)
39 return line
42class AppHeader(QWidget):
43 def __init__(self, tab_bar, app_icon_path, parent=None):
44 super().__init__(parent)
45 self.setObjectName("app_header")
46 # 上の帯の色を QSS で塗るため(素の QWidget は背景を描かない)
47 self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
48 self.setFixedHeight(HEADER_HEIGHT)
49 layout = QHBoxLayout(self)
50 layout.setContentsMargins(8, 0, 12, 0)
51 layout.setSpacing(6)
53 self.menu_button = _header_button("menu-2", tr("メニュー"))
54 self.menu_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
55 # ▼の印を出さない(≡ だけ)
56 self.menu_button.setStyleSheet("QToolButton::menu-indicator { image: none; width: 0; }")
57 layout.addWidget(self.menu_button)
59 self.logo = QLabel()
60 self.logo.setObjectName("header_logo")
61 self.logo.setPixmap(QIcon(app_icon_path).pixmap(22, 22))
62 layout.addWidget(self.logo)
63 self.title_label = QLabel("Graphica")
64 self.title_label.setObjectName("header_title")
65 layout.addWidget(self.title_label)
66 layout.addSpacing(8)
68 self.tab_bar = tab_bar
69 layout.addWidget(tab_bar, 0, Qt.AlignmentFlag.AlignVCenter)
70 self.add_tab_button = _header_button("plus", tr("新しいプロジェクトタブを開く"))
71 self.add_tab_button.setObjectName("add_tab_button")
72 layout.addWidget(self.add_tab_button)
74 layout.addStretch(1)
75 self.search_button = self._make_search_button()
76 layout.addWidget(self.search_button)
77 layout.addStretch(1)
79 self.mode_switch = SegmentedControl()
80 self.mode_switch.setObjectName("mode_switch")
81 for mode in ui_mode.MODES:
82 self.mode_switch.addItem(ui_mode.mode_label(mode), mode)
83 self.mode_switch.setFixedWidth(190)
84 layout.addWidget(self.mode_switch)
85 layout.addWidget(_separator())
87 self.undo_button = _header_button("arrow-back-up", tr("元に戻す"))
88 self.redo_button = _header_button("arrow-forward-up", tr("やり直し"))
89 self.save_button = _header_button("device-floppy", tr("上書き保存"))
90 for button in (self.undo_button, self.redo_button, self.save_button):
91 layout.addWidget(button)
93 # 表示中のタブのクイックアクセス(ピン留め)。タブごとのツールバーを重ねて、前のタブのものだけを見せる
94 self.quick_access_stack = QStackedWidget()
95 self.quick_access_stack.setObjectName("quick_access_slot")
96 self.quick_access_stack.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
97 layout.addWidget(self.quick_access_stack)
99 self.export_button = QPushButton(tr("エクスポート"))
100 self.export_button.setObjectName("header_export_button")
101 self.export_button.setProperty("primaryButton", True)
102 self.export_button.setCursor(Qt.CursorShape.PointingHandCursor)
103 self.export_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
104 self.export_button.setIconSize(QSize(16, 16))
105 self.export_button.setIcon(themed_icon("download", size=16, token="on_accent"))
106 layout.addWidget(self.export_button)
108 def _make_search_button(self):
109 button = QPushButton()
110 button.setObjectName("command_search")
111 button.setCursor(Qt.CursorShape.PointingHandCursor)
112 button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
113 button.setFixedSize(SEARCH_WIDTH, 30)
114 inner = QHBoxLayout(button)
115 inner.setContentsMargins(10, 0, 8, 0)
116 inner.setSpacing(6)
117 self.search_icon = QLabel()
118 self.search_icon.setPixmap(themed_icon("search", size=15, token="text_muted").pixmap(15, 15))
119 text = QLabel(tr("コマンドを検索"))
120 text.setObjectName("command_search_text")
121 shortcut = QLabel(COMMAND_PALETTE_SHORTCUT_TEXT)
122 shortcut.setObjectName("kbd_hint")
123 for label in (self.search_icon, text, shortcut):
124 # ボタンの子のラベルが押下を取らないように(ボタン全体で押せる)
125 label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
126 inner.addWidget(self.search_icon)
127 inner.addWidget(text)
128 inner.addStretch(1)
129 inner.addWidget(shortcut)
130 return button
132 def set_mode(self, mode):
133 blocked = self.mode_switch.blockSignals(True)
134 self.mode_switch.setCurrentIndex(self.mode_switch.findData(mode))
135 self.mode_switch.blockSignals(blocked)
137 def refresh_icons(self):
138 for button in self.findChildren(QToolButton, "header_button") + [self.add_tab_button]:
139 name = button.property("iconName")
140 if name: 140 ↛ 138line 140 didn't jump to line 138 because the condition on line 140 was always true
141 button.setIcon(themed_icon(name))
142 self.search_icon.setPixmap(themed_icon("search", size=15, token="text_muted").pixmap(15, 15))
143 self.export_button.setIcon(themed_icon("download", size=16, token="on_accent"))