Coverage for src/robotide/preferences/editor.py: 74%
172 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
1# Copyright 2008-2015 Nokia Networks
2# Copyright 2016- Robot Framework Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
16"""A generic, extensible preferences dialog
18Usage:
20 dialog = PreferenceEditor(parent, title, preferences, style)
21 dialog.ShowModal()
23preferences is an any object with attribute preferecne_panels, which in turn
24is a list or tuple of classes that inherit from PreferencesPanel.
26style may have any of the values "auto", "notebook", "tree" or
27"single". If style is "auto", the choice of using a single window, a
28notebook, or a tree will depend on how many pages will be in the
29dialog.
31"""
33import builtins 1bc
34import os.path 1bc
36import wx 1bc
37from wx import Colour 1bc
38from wx.lib.scrolledpanel import ScrolledPanel 1bc
40from .settings import RideSettings 1bc
41from ..widgets import ButtonWithHandler 1bc
43_ = wx.GetTranslation # To keep linter/code analyser happy 1bc
44builtins.__dict__['_'] = wx.GetTranslation 1bc
46# any more than TREE_THRESHOLD panels when style is "auto" forces
47# the UI into showing a hierarchical tree
48TREE_THRESHOLD = 5 1bc
49FONT_SIZE = 'font size' 1bc
50FONT_FACE = 'font face' 1bc
53class PreferenceEditor(wx.Dialog): 1bc
54 """A dialog for showing the preference panels"""
55 def __init__(self, parent, title, preferences, style="auto", index=0): 1bc
56 wx.Dialog.__init__(self, parent, wx.ID_ANY, title, size=(850, 700), 1a
57 style=wx.RESIZE_BORDER | wx.DEFAULT_DIALOG_STYLE)
58 # set Left to Right direction (while we don't have localization)
59 self.SetLayoutDirection(wx.Layout_LeftToRight) 1a
60 self.Bind(wx.EVT_CLOSE, self.on_close) 1a
61 self._current_panel = None 1a
62 self._panels = [] 1a
63 self._settings = preferences.settings 1a
64 self._general_settings = self._settings['General'] 1a
65 self.font = self.GetFont() 1a
66 self.font.SetFaceName(self._general_settings[FONT_FACE]) 1a
67 self.font.SetPointSize(self._general_settings[FONT_SIZE]) 1a
68 self.SetFont(self.font) 1a
69 self.SetBackgroundColour(Colour(self._general_settings['background'])) 1a
70 self.SetForegroundColour(Colour(self._general_settings['foreground'])) 1a
71 self._closing = False 1a
73 panels = preferences.preference_panels 1a
74 if style not in ("tree", "notebook", "single", "auto"): 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true1a
75 raise AttributeError("invalid style; must be one of 'tree','notebook','single' or 'auto'")
77 if style == "tree" or (style == "auto" and len(panels) > TREE_THRESHOLD): 77 ↛ 96line 77 didn't jump to line 96 because the condition on line 77 was always true1a
78 self._sw = wx.SplitterWindow(self, wx.ID_ANY, style=wx.SP_LIVE_UPDATE | wx.SP_3D) 1a
79 self._tree = wx.TreeCtrl(self._sw, wx.ID_ANY, style=wx.TR_HIDE_ROOT | wx.TR_HAS_BUTTONS) 1a
80 # create a single container which will hold all the
81 # preference panels
82 self._container = PanelContainer(self._sw, wx.ID_ANY) 1a
83 self._sw.SplitVertically(self._tree, self._container, 210) 1a
84 sizer = wx.BoxSizer(wx.VERTICAL) 1a
85 sizer.Add(self._sw, 1, wx.EXPAND) 1a
86 self._tree.SetFont(self.font) 1a
87 self._tree.SetBackgroundColour(Colour(self._general_settings['background'])) 1a
88 self._tree.SetOwnBackgroundColour(Colour(self._general_settings['secondary background'])) 1a
89 self._tree.SetForegroundColour(Colour(self._general_settings['foreground'])) 1a
90 self._tree.SetOwnForegroundColour(Colour(self._general_settings['secondary foreground'])) 1a
91 self._tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_selection) 1a
92 self._populate_tree(panels) 1a
93 self._tree.SelectItem(self._tree.GetFirstChild(self._tree.GetRootItem())[0]) 1a
94 self.SetSizer(sizer) 1a
96 elif style == "notebook" or (style == "auto" and len(panels) > 1):
97 # the tabs appear in alphabetical order based on their
98 # location. This has the pleasant side effect of "General"
99 # coming before "Plugins", but if some plugin adds a
100 # location of ("aaa","me first!") it will come before
101 # "General". I need some way to order them, though maybe
102 # just special-casing "General" to come first might be
103 # good enough?
104 self._notebook = wx.Notebook(self)
105 for panel_class in sorted(panels, key=lambda p: p.location):
106 # for a notebook, each notebook page gets a container,
107 # and that container will only show one panel
108 container = PanelContainer(self._notebook)
109 panel = container.AddPanel(panel_class, self._settings)
110 container.ShowPanel(panel)
111 self._notebook.AddPage(container, panel.GetTitle())
112 sizer = wx.BoxSizer(wx.VERTICAL)
113 sizer.Add(self._notebook, 1, wx.EXPAND)
114 self.SetSizer(sizer)
116 else:
117 self._container = PanelContainer(self, wx.ID_ANY)
118 sizer = wx.BoxSizer(wx.VERTICAL)
119 sizer.Add(self._container, 1, wx.EXPAND)
120 self.SetSizer(sizer)
122 panel = self._container.AddPanel(panels[index], self._settings)
123 self._container.ShowPanel(panel)
125 def on_close(self, evt): 1bc
126 self._closing = True 1a
127 evt.Skip() 1a
129 def on_tree_selection(self, event): 1bc
130 """Show panel that corresponds to selected tree item
132 Used only when the hierarchical tree is shown.
133 """
134 # On Windows, closing the Dialog causes tree selection events to be
135 # triggered. This is a workaround to ignore those events, which might
136 # try to access dead objects.
137 if self._closing: 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true1a
138 return
139 instance_or_class = self._tree.GetItemData(event.GetItem()) 1a
140 if isinstance(instance_or_class, wx.Panel): 140 ↛ 144line 140 didn't jump to line 144 because the condition on line 140 was always true1a
141 panel = instance_or_class 1a
142 else:
143 # not an instance, assume it's a class
144 panel = self._container.AddPanel(instance_or_class, self._settings)
145 self._panels.append(panel)
146 self._tree.SetItemData(event.GetItem(), panel)
147 self._container.ShowPanel(panel) 1a
149 def _populate_tree(self, panels): 1bc
150 """Recreate the hierarchical tree of preferences panels
152 Used only when the hierarchical tree is shown.
153 """
154 self._tree.AddRoot("Root") 1a
155 for panel_class in panels: 1a
156 location = panel_class.location 1a
157 if not isinstance(location, tuple): 1a
158 # location should be a tuple, but it's easy to accidentally
159 # make it not a tuple (eg: ("Plugins")). This fixes that.
160 location = (location,) 1a
161 item = self._get_item(location) 1a
162 self._tree.SetItemData(item, panel_class) 1a
163 self._tree.ExpandAll() 1a
165 def _get_item(self, location): 1bc
166 item = self._tree.GetRootItem() 1a
167 for text in location: 1a
168 item = self._get_child_item(item, _(text)) 1a
169 return item 1a
171 def _get_child_item(self, parent, text): 1bc
172 """Returns the tree item with the given text under the given parent
174 This will create the item if it doesn't exist
175 """
176 if self._tree.ItemHasChildren(parent): 176 ↛ 183line 176 didn't jump to line 183 because the condition on line 176 was always true1a
177 item, cookie = self._tree.GetFirstChild(parent) 1a
178 while item: 1a
179 if self._tree.GetItemText(item).strip().lower() == text.strip().lower(): 1a
180 return item 1a
181 item, cookie = self._tree.GetNextChild(parent, cookie) 1a
182 # if we get here we didn't find the item
183 item = self._tree.AppendItem(parent, text) 1a
184 return item 1a
186 def _get_children(self, parent): 1bc
187 if self._tree.ItemHasChildren(parent):
188 item, cookie = self._tree.GetFirstChild(parent)
189 while item:
190 yield item
191 item, cookie = self._tree.GetNextChild(parent, cookie)
194class PanelContainer(wx.Panel): 1bc
195 """This contains a preference panel.
197 This container has the ability to hold several panels,
198 and to be able to switch between them. For some modes, however,
199 the container will only hold a single panel.
201 Each page has a title area, and an area for a preferences panel
202 """
203 def __init__(self, *args, **kwargs): 1bc
204 super(PanelContainer, self).__init__(*args, **kwargs) 1a
205 self.parent = self.GetParent() 1a
206 self._current_panel = None 1a
207 self._settings = RideSettings() 1a
208 self.settings = self._settings['General'] 1a
209 self.title = wx.StaticText(self, label="Your message here") 1a
210 hsizer = wx.BoxSizer(wx.HORIZONTAL) 1a
211 config_button = ButtonWithHandler(self, _('Settings'), bitmap='wrench_orange.png', 1a
212 fsize=self.settings[FONT_SIZE],
213 handler=lambda e: self.on_edit_settings(self._settings.user_path))
214 config_button.SetBackgroundColour(self.settings['background']) 1a
215 config_button.SetOwnBackgroundColour(self.settings['background']) 1a
216 config_button.SetForegroundColour(self.settings['foreground']) 1a
217 hsizer.Add(config_button, 0, wx.TOP | wx.RIGHT | wx.EXPAND, 4) 1a
218 hsizer.Add(self.title, 0, wx.TOP | wx.LEFT | wx.EXPAND, 4) 1a
219 self.panels_container = ScrolledPanel(self, wx.ID_ANY, style=wx.TAB_TRAVERSAL) 1a
220 self.panels_container.SetupScrolling() 1a
221 sizer = wx.BoxSizer(wx.VERTICAL) 1a
222 sizer.Add(hsizer) 1a
223 # sizer.Add(self.title, 0, wx.TOP | wx.LEFT | wx.EXPAND, 4)
224 sizer.Add(wx.StaticLine(self), 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 4) 1a
225 sizer.Add(self.panels_container, 1, wx.EXPAND) 1a
226 self.SetSizer(sizer) 1a
227 self.panels_container.SetSizer(wx.BoxSizer(wx.VERTICAL)) 1a
229 font = self.title.GetFont() 1a
230 font.SetFaceName(self.settings[FONT_FACE]) 1a
231 font.SetPointSize(self.settings[FONT_SIZE]) 1a
232 font.MakeLarger() 1a
233 self.title.SetFont(font) 1a
234 self.title.SetForegroundColour(self.settings['foreground']) 1a
235 self.title.SetBackgroundColour(self.settings['background']) 1a
236 self.SetForegroundColour(self.settings['foreground']) 1a
237 self.SetBackgroundColour(self.settings['background']) 1a
239 def AddPanel(self, panel_class, settings): 1bc
240 """Add a panel to the dialog"""
241 panel = panel_class(parent=self.panels_container, settings=settings)
242 self.panels_container.GetSizer().Add(panel, 1, wx.EXPAND)
243 return panel
245 def ShowPanel(self, panel): 1bc
246 """Arrange for the given panel to be shown"""
247 if self._current_panel is not None: 247 ↛ 248line 247 didn't jump to line 248 because the condition on line 247 was never true1a
248 self._current_panel.Hide()
249 self._current_panel = panel 1a
250 panel.SetForegroundColour(self.settings['foreground']) # Critical text all black on 1a
251 panel.SetBackgroundColour(self.settings['background']) # Black background 1a
252 panel.Show() 1a
253 sizer = self.panels_container.GetSizer() 1a
254 item = sizer.GetItem(panel) 1a
255 title = getattr(panel, "title", panel.location[-1]) 1a
256 self.SetTitle(title) 1a
257 font = self.title.GetFont() 1a
258 font.SetFaceName(self.settings[FONT_FACE]) 1a
259 font.SetPointSize(self.settings[FONT_SIZE]) 1a
260 self.SetFont(font) 1a
261 font.MakeLarger() 1a
262 self.title.SetFont(font) 1a
263 self.title.SetForegroundColour(self.settings['foreground']) 1a
264 self.title.SetBackgroundColour(self.settings['background']) 1a
265 if item is None: 265 ↛ 267line 265 didn't jump to line 267 because the condition on line 265 was always true1a
266 sizer.Add(panel, 1, wx.EXPAND) 1a
267 sizer.Layout()
269 def SetTitle(self, title): 1bc
270 """Set the title of the panel"""
271 self.title.SetLabel(title) 1a
273 def on_edit_settings(self, path): 1bc
274 """Starts Text Editor for settings file and closes all if changed"""
275 from ..editor import customsourceeditor
276 from ..context import SETTINGS_DIRECTORY
277 main_settings_path = os.path.join(SETTINGS_DIRECTORY, 'settings.cfg')
278 if path != main_settings_path:
279 customsourceeditor.main(path)
280 else:
281 customsourceeditor.main(main_settings_path)
282 # DEBUG close parent test
283 # self.parent.Close()