Coverage for graphica/gui/app_settings.py: 100.0%
83 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"""アプリの設定(QSettings)のキー・型・既定値の表と、設定を開く唯一の場所。
3読み方は今の挙動をそのまま写している。型を指定しないキー(value_type が None)は INI 形式では真偽や数が文字列で返り、
4要素が 1 つのリストは文字列で返ることがある。どちらも読む側の扱いまで含めて挙動なので、型の有無を変えないこと。
5"""
6from dataclasses import dataclass
7from typing import Any
9from PySide6.QtCore import QSettings
11from graphica.core.i18n import DEFAULT_LANGUAGE
12from graphica.gui.theme_color import DEFAULT_THEME_COLOR
14ORGANIZATION_NAME = "Graphica"
15APPLICATION_NAME = "Graphica"
17DEFAULT_AUTOSAVE_INTERVAL_MIN = 5 # 0 で無効
18# これより点が多いデータセットには点ラベルを描かない(annotate が点の数だけ呼ばれて固まる)。環境設定で変えられる
19DEFAULT_POINT_LABEL_MAX_POINTS = 1000
20DEFAULT_SNAP_TO_GRID_ENABLED = False
21DEFAULT_SNAP_GRID_INTERVAL_PX = 10
23CANVAS_DETACHED_GEOMETRY_KEY = "canvas_detached_geometry"
24CANVAS_WAS_DETACHED_KEY = "canvas_was_detached"
25# 名前を付けて保存するドック配置(起動時に戻す直近の window_state とは別)
26# 閉じている節のキーの JSON 配列。無い・空なら全部開く(節を足しても既定は開いたまま)
27DATASET_PROPERTY_COLLAPSED_SECTIONS_KEY = "dataset_property_collapsed_sections"
28DISABLED_PLUGINS_SETTINGS_KEY = "disabled_plugins"
29QUICK_ACCESS_SETTINGS_KEY = "quick_access_pinned_actions"
30COLOR_PALETTES_SETTINGS_KEY = "custom_color_palettes_json"
31ACTIVE_PALETTE_SETTINGS_KEY = "active_color_palette"
32RECENT_COLORS_SETTINGS_KEY = "recent_colors"
34_TABLE_DEFAULT = object()
37def open_settings() -> QSettings:
38 """アプリの設定を開く。QSettings を作るのはここだけ(テストはこのモジュールの QSettings を差し替えて隔離する)。"""
39 return QSettings(ORGANIZATION_NAME, APPLICATION_NAME)
42@dataclass(frozen=True)
43class Setting:
44 key: str
45 default: Any = None
46 value_type: type | None = None # None なら型を指定せずに読む
47 export_as: type | None = None # 設定の書き出しに入れるときの型(list は要素 1 つを直して読む)
48 export_default: Any = _TABLE_DEFAULT # 書き出しのときだけ既定値が違うもの
50 def read(self, settings: QSettings, default: Any = _TABLE_DEFAULT) -> Any:
51 """default を渡すのは、読む場所によって既定値が違うところ(今の挙動)だけ。"""
52 fallback = self.default if default is _TABLE_DEFAULT else default
53 if self.value_type is None:
54 return settings.value(self.key, fallback)
55 return settings.value(self.key, fallback, type=self.value_type)
57 def write(self, settings: QSettings, value: Any) -> None:
58 settings.setValue(self.key, value)
60 def read_for_export(self, settings: QSettings) -> Any:
61 default = self.default if self.export_default is _TABLE_DEFAULT else self.export_default
62 if self.export_as is list:
63 return as_list(settings.value(self.key, default))
64 return settings.value(self.key, default, type=self.export_as)
67def as_list(value: Any) -> Any:
68 """要素 1 つのリストが文字列で返ったのを直す。空の文字列は空のリスト(設定の書き出し・最近使った色の読み方)。"""
69 if isinstance(value, str):
70 return [value] if value else []
71 return value
74def as_list_keeping_empty_string(value: Any) -> list[Any]:
75 """要素 1 つのリストが文字列で返ったのを直す(最近使ったファイル・クイックアクセスの読み方)。"""
76 if isinstance(value, str):
77 value = [value]
78 return list(value) if value else []
81LANGUAGE = Setting("language", DEFAULT_LANGUAGE, export_as=str, export_default="")
82DARK_MODE = Setting("dark_mode", False, bool, export_as=bool)
83# UI の強調に使う色(利用者が選んだ色そのもの。読みやすさの補正は gui/theme_color.py が毎回する)
84THEME_COLOR = Setting("theme_color", DEFAULT_THEME_COLOR, str, export_as=str)
85# 表示のモード: "lite"(シンプル)/ "advanced"(アドバンスド)。空なら初回に gui/ui_mode.initial_mode() が決める
86UI_MODE = Setting("ui_mode", "", str, export_as=str)
87AUTOSAVE_INTERVAL_MIN = Setting("autosave_interval_min", DEFAULT_AUTOSAVE_INTERVAL_MIN, int, export_as=int)
88AUTOSAVE_DIR = Setting("autosave_dir", "", str)
89# 新しいタブ・プロジェクトに当てる書式テンプレートのパス(空なら使わない)。パスはその PC だけのものなので書き出さない
90DEFAULT_STYLE_TEMPLATE = Setting("default_style_template", "", str)
91# ファイルダイアログを用途ごとに前回のフォルダで開く(gui/dialog_dirs.py)。{用途: フォルダ} の JSON。PC ごとの場所なので書き出さない
92LAST_DIALOG_DIRS = Setting("last_dialog_dirs_json", "", str)
93# プラグインの更新を最後に調べた日(起動時の確認は1日1回)
94PLUGIN_UPDATE_LAST_CHECK = Setting("plugin_update_last_check", "", str)
95POINT_LABEL_MAX_POINTS = Setting("point_label_max_points", DEFAULT_POINT_LABEL_MAX_POINTS, int, export_as=int)
96SNAP_TO_GRID_ENABLED = Setting("snap_to_grid_enabled", DEFAULT_SNAP_TO_GRID_ENABLED, bool, export_as=bool)
97SNAP_GRID_INTERVAL_PX = Setting("snap_grid_interval_px", DEFAULT_SNAP_GRID_INTERVAL_PX, int, export_as=int)
98# 軸の上のホイールで拡大する速さ: "slow" / "normal" / "fast"(gui/tools/view_navigation.WHEEL_ZOOM_BASES)
99WHEEL_ZOOM_SPEED = Setting("wheel_zoom_speed", "normal", str, export_as=str)
100CUSTOM_COLOR_PALETTES = Setting(COLOR_PALETTES_SETTINGS_KEY, "", export_as=str)
101# 画面の既定は配色パレットのダイアログの既定の名前(読む側が渡す)。書き出しでは空
102ACTIVE_COLOR_PALETTE = Setting(ACTIVE_PALETTE_SETTINGS_KEY, None, export_as=str, export_default="")
103QUICK_ACCESS_PINNED_ACTIONS = Setting(QUICK_ACCESS_SETTINGS_KEY, [], export_as=list)
104DISABLED_PLUGINS = Setting(DISABLED_PLUGINS_SETTINGS_KEY, [], export_as=list)
106CLEAN_EXIT = Setting("clean_exit", True, bool)
107HAS_SHOWN_WELCOME = Setting("has_shown_welcome", False, bool)
108MINIMAP_VISIBLE = Setting("minimap_visible", True, bool)
109RECENT_FILES = Setting("recent_files", [])
110RECENT_COLORS = Setting(RECENT_COLORS_SETTINGS_KEY, [])
111WINDOW_GEOMETRY = Setting("window_geometry")
112WINDOW_STATE = Setting("window_state")
113DOCK_LAYOUT_VERSION = Setting("dock_layout_version", 0, int)
114DATASET_PROPERTY_COLLAPSED_SECTIONS = Setting(DATASET_PROPERTY_COLLAPSED_SECTIONS_KEY, "[]")
115CANVAS_WAS_DETACHED = Setting(CANVAS_WAS_DETACHED_KEY, False, bool)
116CANVAS_DETACHED_GEOMETRY = Setting(CANVAS_DETACHED_GEOMETRY_KEY)
118# 設定の書き出し(別の PC や研究室での共有用)に入れるもの。JSON の並びはこの順
119EXPORTED_SETTINGS: tuple[Setting, ...] = (
120 LANGUAGE, DARK_MODE, AUTOSAVE_INTERVAL_MIN, POINT_LABEL_MAX_POINTS, SNAP_TO_GRID_ENABLED, SNAP_GRID_INTERVAL_PX,
121 CUSTOM_COLOR_PALETTES, ACTIVE_COLOR_PALETTE, QUICK_ACCESS_PINNED_ACTIONS, DISABLED_PLUGINS,
122 WHEEL_ZOOM_SPEED, THEME_COLOR, UI_MODE,
123)
126def disabled_plugin_names(settings: QSettings) -> set[str]:
127 """QSettings は要素が1つのリストを文字列で返すことがあるので直す。"""
128 names = DISABLED_PLUGINS.read(settings)
129 if isinstance(names, str):
130 names = [names]
131 return set(names) if names else set()