Skip to content

Commit fe85323

Browse files
authored
Merge pull request #5 from kwrkb/feat/ui-ux-improvements
feat: ダークモード・モノスペースフォント・ショートカット追加
2 parents 40eaa93 + 424e634 commit fe85323

7 files changed

Lines changed: 518 additions & 12 deletions

File tree

PLAN.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SQLiteView 書き込み対応計画
1+
# SQLiteView 開発計画
22

33
## Context
44

@@ -83,3 +83,75 @@ SQLiteView は INSERT/UPDATE/DELETE/CREATE/DROP/BEGIN/COMMIT/ROLLBACK 等の書
8383
- `build_deb.sh`: `tomllib`(Python 3.11+)→ `grep`/`sed` に置換(Python 3.10互換性復元)
8484
- `_strip_sql_noise()`: ダブルクォート識別子の除去を追加(セキュリティ強化)
8585
- エラーパステスト6件追加(空クエリ、無効SQL、未接続操作、存在しないファイル、文字列リテラル内WHERE)
86+
87+
---
88+
89+
## フェーズ2: UI/UX 改善(完了)
90+
91+
### Context
92+
93+
機能は十分だが、UIスタイリングが一切なくシステムネイティブテーマのまま。ダークモード非対応、SQLエディタのフォント未指定、ショートカットも最低限(Ctrl+O/Q のみ)。開発者ツールとしての使い勝手を底上げする。
94+
95+
### 変更対象ファイル
96+
97+
**新規作成:**
98+
- `src/sqliteviewer/theme.py` — Theme enum, QSS読み込み/適用/永続化 (~60行)
99+
- `src/sqliteviewer/resources/light.qss` — ライトテーマ QSS (~180行)
100+
- `src/sqliteviewer/resources/dark.qss` — ダークテーマ QSS (~180行)
101+
102+
**変更:**
103+
- `src/sqliteviewer/app.py` — 起動時テーマ適用 (+3行)
104+
- `src/sqliteviewer/mainwindow.py` — View メニュー、テーマ切替、フォント、ショートカット (+~40行)
105+
- `src/sqliteviewer/sql_highlighter.py` — テーマ連動カラースキーム追加 (+~25行)
106+
107+
### Step 5: テーマ基盤 + ダークモード(完了)
108+
109+
- [x] `theme.py` を作成 — `Theme` enum (LIGHT/DARK)、`load_theme()`/`apply_theme()`/`save_theme_preference()`/`load_theme_preference()`
110+
> `StrEnum`(3.11+) → `str, Enum`(3.10+) に修正済み(`requires-python >= 3.10` 互換)
111+
- [x] `light.qss` を作成 — GitHub風パレット (bg `#ffffff`, fg `#24292e`, accent `#0366d6`)
112+
- 対象: QMainWindow, QTableView, QTabWidget, QListWidget, QPlainTextEdit, QTextEdit, QPushButton, QSplitter, QMenuBar, QMenu, QStatusBar, QScrollBar, QHeaderView
113+
- ボタン hover/pressed 効果、タブ選択インジケーター、スリムスクロールバー、角丸4px
114+
- [x] `dark.qss` を作成 — VS Code dark 風パレット (bg `#1e1e1e`, fg `#d4d4d4`, accent `#4fc1ff`)
115+
- [x] `app.py` を変更 — `window.show()` 前に `apply_theme(load_theme_preference())` を呼ぶ
116+
- [x] `mainwindow.py` に View メニュー追加 — "Toggle Dark Mode" (Ctrl+D)
117+
- [x] `sql_highlighter.py``set_color_scheme()` 追加 — ライト (GitHub風) / ダーク (VS Code風) の2パレットを切替、`rehighlight()` で即反映
118+
- [x] `mainwindow.py` でハイライター参照を保持し、テーマ切替時に色同期
119+
> テーマ設定は `QSettings("SQLiteViewer", "App")``theme` キーへ保存し、起動時に `app.py` で先に適用する。
120+
121+
### Step 6: モノスペースフォント(完了)
122+
123+
- [x] `QFontDatabase.systemFont(SystemFont.FixedFont)` でモノスペースフォント取得、11pt
124+
- [x] `query_editor``schema_view` に適用
125+
- [x] フォント設定後に `setTabStopDistance` を再計算
126+
127+
### Step 7: キーボードショートカット(完了)
128+
129+
- [x] `Ctrl+Enter` / `F5` — クエリ実行 (`QShortcut` で query_editor にバインド)
130+
- [x] `Ctrl+R` — テーブルリフレッシュ (View メニューにアクション追加)
131+
- [x] Run Query ボタンにツールチップ `"Execute SQL (Ctrl+Enter)"` 追加
132+
133+
### 設計判断
134+
135+
- **QSS方式** — QPaletteより柔軟 (hover/角丸/スクロールバー)、外部ライブラリ不要
136+
- **テーマ2種のみ** — Light/Dark。レジストリパターンは over-engineering
137+
- **QFontDatabase.systemFont** — フォント名ハードコードより確実なクロスプラットフォーム対応
138+
- **QShortcut** — eventFilter やサブクラス化より軽量
139+
140+
### 検証方法
141+
142+
```bash
143+
# 回帰テスト
144+
uv run python -m pytest tests/ -v
145+
146+
# 手動テスト
147+
uv run python -m sqliteviewer
148+
# → View > Toggle Dark Mode で切替確認
149+
# → SQLエディタがモノスペースフォントか確認
150+
# → Ctrl+Enter / F5 でクエリ実行確認
151+
# → Ctrl+R でテーブルリフレッシュ確認
152+
# → アプリ再起動後にテーマ維持されるか確認
153+
```
154+
155+
## 現状
156+
157+
フェーズ2まで実装済み。SQLiteView はライト/ダークテーマ切替、固定幅フォントの SQL エディタ/スキーマ表示、`Ctrl+Enter``F5``Ctrl+R` の操作に対応した。レビューで `StrEnum`(3.11+) の Python 3.10 非互換を検出し `str, Enum` に修正済み。

src/sqliteviewer/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from PyQt6.QtWidgets import QApplication
1010

1111
from .mainwindow import MainWindow
12+
from .theme import apply_theme, load_theme_preference
1213

1314

1415
def run(initial_path: Optional[str] = None) -> int:
@@ -22,6 +23,7 @@ def run(initial_path: Optional[str] = None) -> int:
2223
app.setOrganizationName("SQLiteView")
2324
owns_app = True
2425

26+
apply_theme(load_theme_preference(), app)
2527
window = MainWindow()
2628
window.show()
2729

src/sqliteviewer/mainwindow.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Optional
88

99
from PyQt6.QtCore import QSettings, Qt
10-
from PyQt6.QtGui import QAction, QCloseEvent
10+
from PyQt6.QtGui import QAction, QCloseEvent, QFontDatabase, QKeySequence, QShortcut
1111
from PyQt6.QtWidgets import (
1212
QApplication,
1313
QFileDialog,
@@ -32,6 +32,7 @@
3232
from .database import DatabaseError, DatabaseService, QueryResult
3333
from .resources import load_icon
3434
from .sql_highlighter import SqlHighlighter
35+
from .theme import SETTINGS_GROUP, Theme, apply_theme, load_theme_preference, save_theme_preference
3536

3637

3738
MAX_RECENT_FILES = 5
@@ -47,8 +48,9 @@ def __init__(self) -> None:
4748
self.setWindowIcon(load_icon())
4849

4950
self.database_service = DatabaseService()
50-
self.settings = QSettings("SQLiteViewer", "App")
51+
self.settings = QSettings(*SETTINGS_GROUP)
5152
self.query_result: Optional[QueryResult] = None
53+
self.current_theme = load_theme_preference()
5254

5355
self.table_list = QListWidget()
5456
self.table_list.itemSelectionChanged.connect(self._on_table_selected)
@@ -64,8 +66,16 @@ def __init__(self) -> None:
6466

6567
self.query_editor = QPlainTextEdit()
6668
self.query_editor.setPlaceholderText("Write a SQL statement…")
67-
self.query_editor.setTabStopDistance(4 * self.query_editor.fontMetrics().horizontalAdvance(' '))
68-
SqlHighlighter(self.query_editor.document())
69+
self.highlighter = SqlHighlighter(self.query_editor.document())
70+
self.highlighter.set_color_scheme(self.current_theme)
71+
72+
fixed_font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
73+
fixed_font.setPointSize(11)
74+
self.query_editor.setFont(fixed_font)
75+
self.schema_view.setFont(fixed_font)
76+
tab_stop = 4 * self.query_editor.fontMetrics().horizontalAdvance(" ")
77+
self.query_editor.setTabStopDistance(tab_stop)
78+
self.schema_view.setTabStopDistance(tab_stop)
6979

7080
self.query_result_view = QTableView()
7181
self.query_result_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
@@ -117,11 +127,12 @@ def _build_ui(self) -> None:
117127
query_layout.addWidget(self.query_editor)
118128

119129
button_bar = QHBoxLayout()
120-
run_button = QPushButton("Run Query")
121-
run_button.clicked.connect(self._run_query)
130+
self.run_button = QPushButton("Run Query")
131+
self.run_button.setToolTip("Execute SQL (Ctrl+Enter)")
132+
self.run_button.clicked.connect(self._run_query)
122133
export_button = QPushButton("Export Results")
123134
export_button.clicked.connect(self._export_results)
124-
button_bar.addWidget(run_button)
135+
button_bar.addWidget(self.run_button)
125136
button_bar.addWidget(export_button)
126137
button_bar.addStretch(1)
127138
query_layout.addLayout(button_bar)
@@ -159,16 +170,46 @@ def _build_menus(self) -> None:
159170
exit_action.triggered.connect(QApplication.instance().quit)
160171
file_menu.addAction(exit_action)
161172

173+
view_menu = menubar.addMenu("&View")
174+
175+
self.toggle_dark_mode_action = QAction("Toggle Dark Mode", self)
176+
self.toggle_dark_mode_action.setShortcut("Ctrl+D")
177+
self.toggle_dark_mode_action.setCheckable(True)
178+
self.toggle_dark_mode_action.setChecked(self.current_theme == Theme.DARK)
179+
self.toggle_dark_mode_action.toggled.connect(self._toggle_dark_mode)
180+
view_menu.addAction(self.toggle_dark_mode_action)
181+
182+
refresh_action = QAction("Refresh Tables", self)
183+
refresh_action.setShortcut("Ctrl+R")
184+
refresh_action.triggered.connect(self._refresh_tables)
185+
view_menu.addAction(refresh_action)
186+
162187
help_menu = menubar.addMenu("&Help")
163188
about_action = QAction("About", self)
164189
about_action.triggered.connect(self._show_about_dialog)
165190
help_menu.addAction(about_action)
166191

192+
self._install_shortcuts()
193+
194+
def _install_shortcuts(self) -> None:
195+
for shortcut_key in ("Ctrl+Return", "Ctrl+Enter", "F5"):
196+
shortcut = QShortcut(QKeySequence(shortcut_key), self.query_editor)
197+
shortcut.activated.connect(self._run_query)
198+
167199
def _open_dialog(self) -> None:
168200
path, _ = QFileDialog.getOpenFileName(self, "Open SQLite Database", str(Path.home()), "SQLite Database (*.db *.sqlite *.sqlite3);;All Files (*)")
169201
if path:
170202
self.open_database(path)
171203

204+
def _toggle_dark_mode(self, checked: bool) -> None:
205+
self._set_theme(Theme.DARK if checked else Theme.LIGHT)
206+
207+
def _set_theme(self, theme: Theme) -> None:
208+
self.current_theme = theme
209+
apply_theme(theme)
210+
save_theme_preference(theme)
211+
self.highlighter.set_color_scheme(theme)
212+
172213
def open_database(self, path: str) -> None:
173214
try:
174215
self.database_service.open(path)
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
QMainWindow, QWidget {
2+
background-color: #1e1e1e;
3+
color: #d4d4d4;
4+
}
5+
6+
QStatusBar {
7+
background-color: #252526;
8+
color: #9da3a9;
9+
border-top: 1px solid #333333;
10+
}
11+
12+
QMenuBar {
13+
background-color: #252526;
14+
color: #d4d4d4;
15+
border-bottom: 1px solid #333333;
16+
}
17+
18+
QMenuBar::item {
19+
padding: 6px 10px;
20+
background: transparent;
21+
}
22+
23+
QMenuBar::item:selected,
24+
QMenu::item:selected {
25+
background-color: #09395a;
26+
color: #ffffff;
27+
border-radius: 4px;
28+
}
29+
30+
QMenu {
31+
background-color: #252526;
32+
color: #d4d4d4;
33+
border: 1px solid #333333;
34+
padding: 6px;
35+
}
36+
37+
QMenu::item {
38+
padding: 6px 22px 6px 10px;
39+
margin: 2px 0;
40+
}
41+
42+
QMenu::separator {
43+
height: 1px;
44+
background: #333333;
45+
margin: 6px 0;
46+
}
47+
48+
QListWidget,
49+
QTableView,
50+
QPlainTextEdit,
51+
QTextEdit {
52+
background-color: #1e1e1e;
53+
color: #d4d4d4;
54+
border: 1px solid #333333;
55+
border-radius: 4px;
56+
selection-background-color: #264f78;
57+
selection-color: #ffffff;
58+
gridline-color: #2d2d30;
59+
}
60+
61+
QPlainTextEdit,
62+
QTextEdit {
63+
background-color: #1b1b1c;
64+
}
65+
66+
QHeaderView::section {
67+
background-color: #252526;
68+
color: #c5c5c5;
69+
border: none;
70+
border-bottom: 1px solid #333333;
71+
border-right: 1px solid #2d2d30;
72+
padding: 6px 8px;
73+
}
74+
75+
QPushButton {
76+
background-color: #2d2d30;
77+
color: #d4d4d4;
78+
border: 1px solid #3f3f46;
79+
border-radius: 4px;
80+
padding: 6px 12px;
81+
}
82+
83+
QPushButton:hover {
84+
background-color: #35353a;
85+
border-color: #4fc1ff;
86+
}
87+
88+
QPushButton:pressed {
89+
background-color: #41414a;
90+
}
91+
92+
QTabWidget::pane {
93+
border: 1px solid #333333;
94+
border-radius: 4px;
95+
top: -1px;
96+
}
97+
98+
QTabBar::tab {
99+
background-color: #252526;
100+
color: #9da3a9;
101+
padding: 8px 14px;
102+
margin-right: 4px;
103+
border-top-left-radius: 4px;
104+
border-top-right-radius: 4px;
105+
}
106+
107+
QTabBar::tab:selected {
108+
background-color: #1e1e1e;
109+
color: #ffffff;
110+
border-bottom: 2px solid #4fc1ff;
111+
}
112+
113+
QTabBar::tab:hover:!selected {
114+
background-color: #2d2d30;
115+
}
116+
117+
QSplitter::handle {
118+
background-color: #333333;
119+
width: 2px;
120+
height: 2px;
121+
}
122+
123+
QScrollBar:vertical {
124+
background: transparent;
125+
width: 10px;
126+
margin: 2px;
127+
}
128+
129+
QScrollBar:horizontal {
130+
background: transparent;
131+
height: 10px;
132+
margin: 2px;
133+
}
134+
135+
QScrollBar::handle:vertical,
136+
QScrollBar::handle:horizontal {
137+
background: #4a4a4f;
138+
border-radius: 4px;
139+
min-height: 24px;
140+
min-width: 24px;
141+
}
142+
143+
QScrollBar::handle:vertical:hover,
144+
QScrollBar::handle:horizontal:hover {
145+
background: #5a5a60;
146+
}
147+
148+
QScrollBar::add-line,
149+
QScrollBar::sub-line,
150+
QScrollBar::add-page,
151+
QScrollBar::sub-page {
152+
background: none;
153+
border: none;
154+
}

0 commit comments

Comments
 (0)