Coverage for src/hods/tui/base/edit.py: 100.00%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""hods - home directory synchronization.
3Copyright (C) 2016-2020 Mathias Stelzer <knoppo@rolln.de>
5hods is free software: you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation, either version 3 of the License, or
8(at your option) any later version.
10hods is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
15You should have received a copy of the GNU General Public License
16along with this program. If not, see <http://www.gnu.org/licenses/>.
17"""
18import urwid
20from hods.tui.base.list import button_list
21from hods.tui.base.view import ErrorWindow, ListBoxWindow
22from hods.tui.base.widgets import FocusMap
25class ValidationError(Exception):
26 """An `Exception` to easily cancel saving.
28 It displays a user-friendly error message in a dialog.
29 """
31 def __init__(self, message):
32 """Initialize error.
34 :param message: `str` - User-friendly error message
35 """
36 super().__init__(message)
37 self.message = message
40class EditWindow(ListBoxWindow):
41 """Base window to show forms.
43 It takes a list of widgets to display and appends *Save* and
44 *Cancel* buttons. Pressing them will close the window by default.
46 Overwrite `save` to add an action to the *Save* button.
47 Use `ValidationError` when validating your data to show
48 an Error dialog.
50 .. note:: `on_close` is called *before* `on_save` and `on_abort`.
51 """
53 ValidationError = ValidationError
55 save_button_text = 'Save'
57 close_on_save = True
59 def __init__(self, app, **kwargs):
60 """Initialize widget.
62 Display given items and *Save* and *Cancel* buttons in a `urwid.ListBox`.
64 :param app: `hods.app.App` instance
65 :param items: widgets to display
66 :param kwargs: Additional options, passed to `Window`
67 """
68 self._on_save = kwargs.pop('on_save', None)
69 self._defer_save = kwargs.pop('defer_save', True)
70 self._on_abort = kwargs.pop('on_abort', None)
71 self._defer_abort = kwargs.pop('defer_abort', True)
73 self.buttons = [
74 (urwid.Button(self.save_button_text, on_press=self._on_press_save), 'success'),
75 (urwid.Button('Cancel', on_press=lambda btn: self.close()), 'warning'),
76 ]
78 additional_buttons = self.get_additional_buttons()
79 if additional_buttons is not None: # pragma: no branch
80 self.buttons.append(urwid.Divider())
81 self.buttons.extend(additional_buttons)
83 super().__init__(app, **kwargs)
85 self.saved = False
86 self.save_args = ()
88 def get_body(self):
89 """Get the main widget to display in this window."""
90 self.columns = urwid.Columns([
91 self.wrap_body(super().get_body()),
92 button_list(self.buttons, column=True),
93 ], dividechars=1)
94 return self.columns
96 def wrap_body(self, body):
97 """Overwrite to wrap the body listbox widget."""
98 return body
100 def get_additional_buttons(self):
101 """Overwrite to change displayed buttons."""
102 return []
104 def _on_press_save(self, button):
105 """Call `save` when *Save* button is pressed.
107 If a `ValidationError` occurs, display an
108 `ErrorWindow` with the error message.
109 """
110 try:
111 data = self.clean()
112 except self.ValidationError as e:
113 ErrorWindow(self.app, e.message, title='Validation Error', parent=self).show()
114 return
116 args = self.save(data)
117 if args is None:
118 args = ()
119 elif not isinstance(args, tuple): # pragma: no branch
120 args = (args,)
122 self.saved = True
123 self.save_args = args
124 if self.close_on_save:
125 self.close()
127 def on_save(self):
128 """Called when closing the window after successful save."""
129 self.app.check_defer(self._on_save, *self.save_args, defer=self._defer_save)
131 def on_abort(self):
132 """Called when closing the window without a successful save."""
133 self.app.check_defer(self._on_abort, defer=self._defer_abort)
135 def on_close(self):
136 """Called by the app before switching to another widget."""
137 super().on_close()
138 if self.saved:
139 self.on_save()
140 else:
141 self.on_abort()
143 def clean(self):
144 """Overwrite to collect and validate data.
146 Raise `hods.widgets.window.EditWindow.ValidationError`
147 to display an error message dialog.
148 """
149 return {}
151 def save(self, data):
152 """Overwrite to store validated data."""
153 return data
156def radio_buttons(choices, initial=None, on_change=None, getset=None, group=None, columns=False):
157 """Build a radio button group for the given choices."""
158 if group is None: # pragma: no branch
159 group = []
161 if getset is not None:
162 obj, attr = getset
163 initial = getattr(obj, attr)
164 _on_change = on_change
166 def on_change(btn, state, user_data=None):
167 if state:
168 setattr(obj, attr, user_data)
169 if callable(_on_change):
170 _on_change(btn, state, user_data=user_data)
172 for label, value in choices:
173 btn = urwid.RadioButton(
174 group,
175 label,
176 state=initial == value,
177 on_state_change=on_change,
178 user_data=value)
180 if columns:
181 yield len(label) + 4, btn
182 else:
183 yield btn
186class ObjectEditWindow(EditWindow):
187 """A window to edit object attributes."""
189 def __init__(self, app, obj, **kwargs):
190 """Initialize window widget.
192 :param app: `hods.tui.__main__.App` object. Passed to
193 mixed class!
194 :param obj: The object to edit.
195 :param kwargs: Pass to mixed class.
196 """
197 self.object = obj
198 super().__init__(app, **kwargs)
200 def save(self, data):
201 """Store the attributes in cleaned data and return the object."""
202 for name, value in data.items():
203 setattr(self.object, name, value)
204 self.app.save()
205 self.app.refresh()
206 return self.object
209class ObjectDeleteMixin:
210 """Mixin to add a delete button to a ObjectEditWindow."""
212 def get_additional_buttons(self):
213 """Return the additional delete button."""
214 yield from super().get_additional_buttons()
215 yield urwid.Button('Delete', on_press=self.delete), 'error'
217 def get_delete_choices(self):
218 """Return the choices to display in the delete dialog."""
219 yield 'delete', 'Delete', 'warning'
220 yield 'cancel', 'Cancel', 'warning'
222 def get_delete_text(self):
223 """Return the message to display in the delete dialog."""
224 dir_text = ''
225 if self.object.isdir():
226 dir_text = '\nand all of its contents'
227 return 'Are you sure you want to delete:\n"{}"{}?'.format(self.object, dir_text)
229 def delete(self, user_data=None):
230 """Show the delete dialog."""
231 ChoiceWindow(
232 self.app,
233 text=self.get_delete_text(),
234 choices=list(self.get_delete_choices()),
235 on_select=self.on_delete_select,
236 attr='error',
237 title='Delete',
238 height=8,
239 parent=self,
240 ).show()
242 def on_delete_select(self, choice):
243 """Delete the object and close the window if delete was selected."""
244 if choice == 'delete':
245 self.object.delete()
246 self.app.save()
247 self.close()
250class ChoiceWindow(ListBoxWindow):
251 """A base choice window that closes on select."""
253 def __init__(self, app, choices, **kwargs):
254 """Initialize widget.
256 :param app: `App` instance
257 :param choices: list of tuples containing the id, label, and
258 style-attribute for each choice
259 :param kwargs: Additional options, passed to `Window`
260 """
261 self.choices = choices
263 self.text = kwargs.pop('text', '')
264 self.focus_column = kwargs.pop('focus_column', None)
265 self.stretch = kwargs.pop('stretch', False)
266 self.vertical = kwargs.pop('vertical', False)
267 if self.vertical:
268 self.stretch = False
270 self._on_select = kwargs.pop('on_select')
272 kwargs.setdefault('height', len(self.text.splitlines()) + 5)
273 super().__init__(app, **kwargs)
275 def get_items(self):
276 """Collect widgets to display."""
277 yield urwid.Text(self.text, align=urwid.CENTER)
278 yield urwid.Divider()
279 button_widgets = list(self.get_choice_widgets())
280 if self.vertical:
281 yield urwid.Pile(button_widgets, focus_item=self.focus_column)
282 else:
283 yield urwid.Columns(button_widgets, dividechars=1, focus_column=self.focus_column)
285 def get_choice_widgets(self):
286 """Collect choice button widgets."""
287 if not self.stretch and not self.vertical:
288 yield urwid.Text('')
290 for options in self.choices:
291 if len(options) == 3:
292 user_data, label, attr = options
293 else:
294 user_data, label = options
295 attr = 'edit'
297 btn = urwid.Button(str(label), on_press=self.on_choice_press, user_data=str(user_data))
298 widget = FocusMap(btn, attr)
300 if self.vertical:
301 widget = urwid.Padding(widget, align='center')
302 elif not self.stretch: # pragma: no branch # unused
303 widget = len(str(label)) + 4, widget
305 yield widget
307 if self.vertical:
308 yield urwid.Divider()
310 if not self.stretch and not self.vertical:
311 yield urwid.Text('')
313 def on_choice_press(self, btn, user_data):
314 """Close the window and call the callback with the selected option."""
315 self.close()
316 self._on_select(user_data)