1#!/usr/local/bin/python3.8
2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
3
4__license__   = 'GPL v3'
5__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
6__docformat__ = 'restructuredtext en'
7
8import weakref, textwrap
9
10from qt.core import (
11    QWidget, QLabel, QTabWidget, QGridLayout, QLineEdit, QVBoxLayout,
12    QGroupBox, QComboBox, QSizePolicy, QDialog, QDialogButtonBox, QCheckBox,
13    QSpacerItem)
14
15from calibre.ebooks import BOOK_EXTENSIONS
16from calibre.gui2.device_drivers.mtp_config import (FormatsConfig, TemplateConfig)
17from calibre.devices.usbms.driver import debug_print
18
19
20def wrap_msg(msg):
21    return textwrap.fill(msg.strip(), 100)
22
23
24def setToolTipFor(widget, tt):
25    widget.setToolTip(wrap_msg(tt))
26
27
28def create_checkbox(title, tt, state):
29    cb = QCheckBox(title)
30    cb.setToolTip(wrap_msg(tt))
31    cb.setChecked(bool(state))
32    return cb
33
34
35class TabbedDeviceConfig(QTabWidget):
36    """
37    This is a generic Tabbed Device config widget. It designed for devices with more
38    complex configuration. But, it is backwards compatible to the standard device
39    configuration widget.
40
41    The configuration made up of two default tabs plus extra tabs as needed for the
42    device. The extra tabs are defined as part of the subclass of this widget for
43    the device.
44
45    The two default tabs are the "File Formats" and "Extra Customization". These
46    tabs are the same as the two sections of the standard device configuration
47    widget. The second of these tabs will only be created if the device driver has
48    extra configuration options. All options on these tabs work the same way as for
49    the standard device configuration widget.
50
51    When implementing a subclass for a device driver, create tabs, subclassed from
52    DeviceConfigTab, for each set of options. Within the tabs, group boxes, subclassed
53    from DeviceOptionsGroupBox, are created to further group the options. The group
54    boxes can be coded to support any control type and dependencies between them.
55    """
56
57    def __init__(self, device_settings, all_formats, supports_subdirs,
58                    must_read_metadata, supports_use_author_sort,
59                    extra_customization_message, device,
60                    extra_customization_choices=None, parent=None):
61        QTabWidget.__init__(self, parent)
62        self._device = weakref.ref(device)
63
64        self.device_settings = device_settings
65        self.all_formats = set(all_formats)
66        self.supports_subdirs = supports_subdirs
67        self.must_read_metadata = must_read_metadata
68        self.supports_use_author_sort = supports_use_author_sort
69        self.extra_customization_message = extra_customization_message
70        self.extra_customization_choices = extra_customization_choices
71
72        try:
73            self.device_name = device.get_gui_name()
74        except TypeError:
75            self.device_name = getattr(device, 'gui_name', None) or _('Device')
76
77        if device.USER_CAN_ADD_NEW_FORMATS:
78            self.all_formats = set(self.all_formats) | set(BOOK_EXTENSIONS)
79
80        self.base = QWidget(self)
81#         self.insertTab(0, self.base, _('Configure %s') % self.device.current_friendly_name)
82        self.insertTab(0, self.base, _("File formats"))
83        l = self.base.l = QGridLayout(self.base)
84        self.base.setLayout(l)
85
86        self.formats = FormatsConfig(self.all_formats, device_settings.format_map)
87        if device.HIDE_FORMATS_CONFIG_BOX:
88            self.formats.hide()
89
90        self.opt_use_subdirs = create_checkbox(
91                                           _("Use sub-folders"),
92                                           _('Place files in sub-folders if the device supports them'),
93                                           device_settings.use_subdirs
94                                           )
95        self.opt_read_metadata = create_checkbox(
96                                             _("Read metadata from files on device"),
97                                             _('Read metadata from files on device'),
98                                             device_settings.read_metadata
99                                             )
100
101        self.template = TemplateConfig(device_settings.save_template)
102        self.opt_use_author_sort = create_checkbox(
103                                             _("Use author sort for author"),
104                                             _("Use author sort for author"),
105                                             device_settings.read_metadata
106                                             )
107        self.opt_use_author_sort.setObjectName("opt_use_author_sort")
108        self.base.la = la = QLabel(_(
109            'Choose the formats to send to the %s')%self.device_name)
110        la.setWordWrap(True)
111
112        l.addWidget(la,                         1, 0, 1, 1)
113        l.addWidget(self.formats,               2, 0, 1, 1)
114        l.addWidget(self.opt_read_metadata,     3, 0, 1, 1)
115        l.addWidget(self.opt_use_subdirs,       4, 0, 1, 1)
116        l.addWidget(self.opt_use_author_sort,   5, 0, 1, 1)
117        l.addWidget(self.template,              6, 0, 1, 1)
118        l.setRowStretch(2, 10)
119
120        if device.HIDE_FORMATS_CONFIG_BOX:
121            self.formats.hide()
122
123        if supports_subdirs:
124            self.opt_use_subdirs.setChecked(device_settings.use_subdirs)
125        else:
126            self.opt_use_subdirs.hide()
127        if not must_read_metadata:
128            self.opt_read_metadata.setChecked(device_settings.read_metadata)
129        else:
130            self.opt_read_metadata.hide()
131        if supports_use_author_sort:
132            self.opt_use_author_sort.setChecked(device_settings.use_author_sort)
133        else:
134            self.opt_use_author_sort.hide()
135
136        self.extra_tab = ExtraCustomization(self.extra_customization_message,
137                                            self.extra_customization_choices,
138                                            self.device_settings)
139        # Only display the extra customization tab if there are options on it.
140        if self.extra_tab.has_extra_customizations:
141            self.addTab(self.extra_tab, _('Extra customization'))
142
143        self.setCurrentIndex(0)
144
145    def addDeviceTab(self, tab, label):
146        '''
147        This is used to add a new tab for the device config. The new tab will always be added
148        as immediately before the "Extra Customization" tab.
149        '''
150        extra_tab_pos = self.indexOf(self.extra_tab)
151        self.insertTab(extra_tab_pos, tab, label)
152
153    def __getattr__(self, attr_name):
154        "If the object doesn't have an attribute, then check each tab."
155        try:
156            return super().__getattr__(attr_name)
157        except AttributeError as ae:
158            for i in range(0, self.count()):
159                atab = self.widget(i)
160                try:
161                    return getattr(atab, attr_name)
162                except AttributeError:
163                    pass
164            raise ae
165
166    @property
167    def device(self):
168        return self._device()
169
170    def format_map(self):
171        return self.formats.format_map
172
173    def use_subdirs(self):
174        return self.opt_use_subdirs.isChecked()
175
176    def read_metadata(self):
177        return self.opt_read_metadata.isChecked()
178
179    def use_author_sort(self):
180        return self.opt_use_author_sort.isChecked()
181
182    @property
183    def opt_save_template(self):
184        # Really shouldn't be accessing the template this way
185        return self.template.t
186
187    def text(self):
188        # Really shouldn't be accessing the template this way
189        return self.template.t.text()
190
191    @property
192    def opt_extra_customization(self):
193        return self.extra_tab.opt_extra_customization
194
195    @property
196    def label(self):
197        return self.opt_save_template
198
199    def validate(self):
200        if hasattr(self, 'formats'):
201            if not self.formats.validate():
202                return False
203            if not self.template.validate():
204                return False
205        return True
206
207    def commit(self):
208        debug_print("TabbedDeviceConfig::commit: start")
209        p = self.device._configProxy()
210
211        p['format_map'] = self.formats.format_map
212        p['use_subdirs'] = self.use_subdirs()
213        p['read_metadata'] = self.read_metadata()
214        p['save_template'] = self.template.template
215        p['extra_customization'] = self.extra_tab.extra_customization()
216
217        return p
218
219
220class DeviceConfigTab(QWidget):  # {{{
221    '''
222    This is an abstraction for a tab in the configuration. The main reason for it is to
223    abstract the properties of the configuration tab. When a property is accessed, it
224    will iterate over all known widgets looking for the property.
225    '''
226
227    def __init__(self, parent=None):
228        QWidget.__init__(self)
229        self.parent = parent
230
231        self.device_widgets = []
232
233    def addDeviceWidget(self, widget):
234        self.device_widgets.append(widget)
235
236    def __getattr__(self, attr_name):
237        try:
238            return super().__getattr__(attr_name)
239        except AttributeError as ae:
240            for awidget in self.device_widgets:
241                try:
242                    return getattr(awidget, attr_name)
243                except AttributeError:
244                    pass
245            raise ae
246
247
248class ExtraCustomization(DeviceConfigTab):  # {{{
249
250    def __init__(self, extra_customization_message, extra_customization_choices, device_settings):
251        super().__init__()
252
253        debug_print("ExtraCustomization.__init__ - extra_customization_message=", extra_customization_message)
254        debug_print("ExtraCustomization.__init__ - extra_customization_choices=", extra_customization_choices)
255        debug_print("ExtraCustomization.__init__ - device_settings.extra_customization=", device_settings.extra_customization)
256        debug_print("ExtraCustomization.__init__ - device_settings=", device_settings)
257        self.extra_customization_message = extra_customization_message
258
259        self.l = QVBoxLayout(self)
260        self.setLayout(self.l)
261
262        options_group = QGroupBox(_("Extra driver customization options"), self)
263        self.l.addWidget(options_group)
264        self.extra_layout = QGridLayout()
265        self.extra_layout.setObjectName("extra_layout")
266        options_group.setLayout(self.extra_layout)
267
268        if extra_customization_message:
269            extra_customization_choices = extra_customization_choices or {}
270
271            def parse_msg(m):
272                msg, _, tt = m.partition(':::') if m else ('', '', '')
273                return msg.strip(), textwrap.fill(tt.strip(), 100)
274
275            if isinstance(extra_customization_message, list):
276                self.opt_extra_customization = []
277                if len(extra_customization_message) > 6:
278                    row_func = lambda x, y: ((x//2) * 2) + y
279                    col_func = lambda x: x%2
280                else:
281                    row_func = lambda x, y: x*2 + y
282                    col_func = lambda x: 0
283
284                for i, m in enumerate(extra_customization_message):
285                    label_text, tt = parse_msg(m)
286                    if not label_text:
287                        self.opt_extra_customization.append(None)
288                        continue
289                    if isinstance(device_settings.extra_customization[i], bool):
290                        self.opt_extra_customization.append(QCheckBox(label_text))
291                        self.opt_extra_customization[-1].setToolTip(tt)
292                        self.opt_extra_customization[i].setChecked(bool(device_settings.extra_customization[i]))
293                    elif i in extra_customization_choices:
294                        cb = QComboBox(self)
295                        self.opt_extra_customization.append(cb)
296                        l = QLabel(label_text)
297                        l.setToolTip(tt), cb.setToolTip(tt), l.setBuddy(cb), cb.setToolTip(tt)
298                        for li in sorted(extra_customization_choices[i]):
299                            self.opt_extra_customization[i].addItem(li)
300                        cb.setCurrentIndex(max(0, cb.findText(device_settings.extra_customization[i])))
301                    else:
302                        self.opt_extra_customization.append(QLineEdit(self))
303                        l = QLabel(label_text)
304                        l.setToolTip(tt)
305                        self.opt_extra_customization[i].setToolTip(tt)
306                        l.setBuddy(self.opt_extra_customization[i])
307                        l.setWordWrap(True)
308                        self.opt_extra_customization[i].setText(device_settings.extra_customization[i])
309                        self.opt_extra_customization[i].setCursorPosition(0)
310                        self.extra_layout.addWidget(l, row_func(i + 2, 0), col_func(i))
311                    self.extra_layout.addWidget(self.opt_extra_customization[i],
312                                                row_func(i + 2, 1), col_func(i))
313                spacerItem1 = QSpacerItem(10, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
314                self.extra_layout.addItem(spacerItem1, row_func(i + 2 + 2, 1), 0, 1, 2)
315                self.extra_layout.setRowStretch(row_func(i + 2 + 2, 1), 2)
316            else:
317                self.opt_extra_customization = QLineEdit()
318                label_text, tt = parse_msg(extra_customization_message)
319                l = QLabel(label_text)
320                l.setToolTip(tt)
321                l.setBuddy(self.opt_extra_customization)
322                l.setWordWrap(True)
323                if device_settings.extra_customization:
324                    self.opt_extra_customization.setText(device_settings.extra_customization)
325                    self.opt_extra_customization.setCursorPosition(0)
326                self.opt_extra_customization.setCursorPosition(0)
327                self.extra_layout.addWidget(l, 0, 0)
328                self.extra_layout.addWidget(self.opt_extra_customization, 1, 0)
329
330    def extra_customization(self):
331        ec = []
332        if self.extra_customization_message:
333            if isinstance(self.extra_customization_message, list):
334                for i in range(0, len(self.extra_customization_message)):
335                    if self.opt_extra_customization[i] is None:
336                        ec.append(None)
337                        continue
338                    if hasattr(self.opt_extra_customization[i], 'isChecked'):
339                        ec.append(self.opt_extra_customization[i].isChecked())
340                    elif hasattr(self.opt_extra_customization[i], 'currentText'):
341                        ec.append(str(self.opt_extra_customization[i].currentText()).strip())
342                    else:
343                        ec.append(str(self.opt_extra_customization[i].text()).strip())
344            else:
345                ec = str(self.opt_extra_customization.text()).strip()
346                if not ec:
347                    ec = None
348
349        return ec
350
351    @property
352    def has_extra_customizations(self):
353        debug_print("ExtraCustomization::has_extra_customizations - self.extra_customization_message", self.extra_customization_message)
354        return self.extra_customization_message and len(self.extra_customization_message) > 0
355
356# }}}
357
358
359class DeviceOptionsGroupBox(QGroupBox):
360    """
361    This is a container for the individual options for a device driver.
362    """
363
364    def __init__(self, parent, device=None, title=_("Unknown")):
365        QGroupBox.__init__(self, parent)
366
367        self.device = device
368        self.setTitle(title)
369
370
371if __name__ == '__main__':
372    from calibre.gui2 import Application
373    from calibre.devices.kobo.driver import KOBO
374    from calibre.devices.scanner import DeviceScanner
375    s = DeviceScanner()
376    s.scan()
377    app = Application([])
378    dev = KOBO(None)
379    debug_print("KOBO:", KOBO)
380#     dev.startup()
381#     cd = dev.detect_managed_devices(s.devices)
382#     dev.open(cd, 'test')
383    cw = dev.config_widget()
384    d = QDialog()
385    d.l = QVBoxLayout()
386    d.setLayout(d.l)
387    d.l.addWidget(cw)
388    bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok|QDialogButtonBox.StandardButton.Cancel)
389    d.l.addWidget(bb)
390    bb.accepted.connect(d.accept)
391    bb.rejected.connect(d.reject)
392    if d.exec() == QDialog.DialogCode.Accepted:
393        cw.commit()
394    dev.shutdown()
395