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

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. 

15 

16"""A generic, extensible preferences dialog 

17 

18Usage: 

19 

20 dialog = PreferenceEditor(parent, title, preferences, style) 

21 dialog.ShowModal() 

22 

23preferences is an any object with attribute preferecne_panels, which in turn 

24is a list or tuple of classes that inherit from PreferencesPanel. 

25 

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. 

30 

31""" 

32 

33import builtins 1bc

34import os.path 1bc

35 

36import wx 1bc

37from wx import Colour 1bc

38from wx.lib.scrolledpanel import ScrolledPanel 1bc

39 

40from .settings import RideSettings 1bc

41from ..widgets import ButtonWithHandler 1bc

42 

43_ = wx.GetTranslation # To keep linter/code analyser happy 1bc

44builtins.__dict__['_'] = wx.GetTranslation 1bc

45 

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

51 

52 

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

72 

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'") 

76 

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

95 

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) 

115 

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) 

121 

122 panel = self._container.AddPanel(panels[index], self._settings) 

123 self._container.ShowPanel(panel) 

124 

125 def on_close(self, evt): 1bc

126 self._closing = True 1a

127 evt.Skip() 1a

128 

129 def on_tree_selection(self, event): 1bc

130 """Show panel that corresponds to selected tree item 

131 

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

148 

149 def _populate_tree(self, panels): 1bc

150 """Recreate the hierarchical tree of preferences panels 

151 

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

164 

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

170 

171 def _get_child_item(self, parent, text): 1bc

172 """Returns the tree item with the given text under the given parent 

173 

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

185 

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) 

192 

193 

194class PanelContainer(wx.Panel): 1bc

195 """This contains a preference panel. 

196 

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. 

200 

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

228 

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

238 

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 

244 

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() 

268 

269 def SetTitle(self, title): 1bc

270 """Set the title of the panel""" 

271 self.title.SetLabel(title) 1a

272 

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()