summaryrefslogtreecommitdiff
path: root/kberylsettings/contentframe.py
blob: 3c709e769fd50dea89c183e07cfb594de63f8806 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" kberylsettings.contentframe -> defines the right side about/setting views.

"""
from kdecore import KIcon, i18n
from kdeui import KDialog, KMessageBox, KPassivePopup, \
     KSeparator, KStdGuiItem, KTabWidget
from qt import Qt, QFrame, QHBoxLayout, QLabel, QScrollView, QSize, \
     QSizePolicy, QStringList, QToolTip, QVBoxLayout

from kberylsettings.aboutpage import AboutPage
from kberylsettings.beryl import Setting
from kberylsettings.lib import Signals, icon, iconLoader, iconSet
from kberylsettings.widget import Frame, SmallPushButton, WidgetStack, guiButton
from kberylsettings.settingwidget import settingWidget


marginHint = KDialog.marginHint()
spacingHint = KDialog.spacingHint()


class ContentFrame(WidgetStack):
    """ ContentFrame -> multiple displays of plugin information

    This widget stack builds and manages three types of pages:

        1.  an 'about' page for displaying plugin information.

        2.  a 'single' page for displaying a setting or a
        sequence of settings.
      
        3.  a 'multiple' page for displaying settings in groups
        via a tabbed interface.
        
    """
    aboutPageId, singlePageId, tabPageId = range(3)

    def __init__(self, parent):
        WidgetStack.__init__(self, parent)
        self.aboutPage = aboutPage = AboutPage(self)
        self.singlePage = singlePage = SinglePage(self)
        self.tabPage = tabPage = TabPage(self)

        addWidget = self.addWidget
        addWidget(aboutPage, self.aboutPageId)
        addWidget(singlePage, self.singlePageId)
        addWidget(tabPage, self.tabPageId)
        
        root = self.topLevelWidget()
        connect = self.connect
        connect(root, Signals.showAbout, self.showAboutPage)        
        connect(root, Signals.showSettings, self.showSinglePage)
        connect(root, Signals.showGroups, self.showTabPage)
        connect(aboutPage, Signals.selectPrevious,
                root, Signals.selectPrevious)
        
    def showAboutPage(self, context, plugin):
        """ displays the about page

        @param plugin berylsettings Plugin instance
        @return None
        """
        self.aboutPage.showAbout(context, plugin)
        self.raiseWidget(self.aboutPageId)

    def showSinglePage(self, plugin, arg):
        """ displays the page for a single type of setting

        @param plugin berylsettings Plugin instance
        @return None
        """
        self.singlePage.showSettings(plugin, arg)        
        self.raiseWidget(self.singlePageId)

    def showTabPage(self, plugin):
        """ displays the page for groups of settings

        @param plugin berylsettings Plugin instance
        @return None
        """
        self.tabPage.setGroups(plugin)
        self.raiseWidget(self.tabPageId)

    def queryClose(self):
        """ checks for unsaved changes in a setting page

        @return True if no changes, changes discarded, or changes
        applied; False if canceled
        """
        try:
            w = self.visibleWidget()
            return w.unsavedCheck(None, None)
        except (AttributeError, ), exc:
            return True


class SettingPage:
    """ SettingPage -> mixin with methods common to both the
    SinglePage and the TabPage types.

    """
    previous = (None, None)
    unsavedText = ('There are unsaved changes in the active settings.\n'
                   'Do you want to apply the changes before changing views '
                   'or discard the changes?')

    def buildConnections(self):
        """ build the connections for this instance

        @return None
        """
        connect = self.connect
        connect(self.footer.helpButton, Signals.clicked, self.settingHelp)
        connect(self.footer.defaultsButton, Signals.clicked, self.settingDefaults)
        connect(self.footer.resetButton, Signals.clicked, self.settingsReset)
        connect(self.footer.applyButton, Signals.clicked, self.settingApply)        
        root = self.topLevelWidget()
        connect(self, Signals.berylSettingChanged, root.onContextChanged)
        connect(self, Signals.statusMessage, root.showMessage)
        connect(self, Signals.selectPrevious, root, Signals.selectPrevious)

    def onSomethingChanged(self):
        """ enable apply and reset buttons on setting value change

        @return None
        """
        self.enableApplyReset(True, True)

    def enableApplyReset(self, enableApply, enableReset):
        """ convenience for setting enabled state of apply and reset buttons

        @return None
        """
        self.footer.applyButton.setEnabled(enableApply)
        self.footer.resetButton.setEnabled(enableReset)

    def unsaved(self):
        """ checks for unsaved items (defers to apply button enabled state)

        @return True if unsaved settings
        """
        return self.footer.applyButton.isEnabled()

    def unsavedDialog(self):
        """ display a dialog warning of unsaved changes

        @return KMessageBox.Yes, .No, or .Cancel
        """
        msg = KMessageBox.warningYesNoCancel
        return msg(self, self.unsavedText,
                   i18n('Unsaved Changes'),
                   KStdGuiItem.apply(),
                   KStdGuiItem.discard())

    def unsavedCheck(self, plugin, arg):
        """ checks for unsaved items and prompts for action if any

        @param plugin berylsettings Plugin instance
        @param arg setting name or setting section name
        @return True if no save needed, changes discarded or applied;
        False otherwise
        """
        ret = True
        if self.unsaved():
            res = self.unsavedDialog()
            if res == KMessageBox.Cancel:
                self.emit(Signals.selectPrevious, self.previous)
                ret = False
            elif res == KMessageBox.Yes:
                self.settingApply()
            else:
                self.settingDiscard()
        if ret:
            self.previous = (plugin, arg)
        return ret

    def settingHelp(self):
        """ not implemented

        """

    def settingDiscard(self):
        """ unsaved changes discarded; disable apply and reset buttons

        @return None
        """
        self.enableApplyReset(False, False)

    def settingDefaults(self):
        """ set each setting to its default value

        @return None
        """
        excs = []
        for w in self.queryList('SettingWidget'):
            try:
                w.setDefault()
            except (Exception, ), exc:
                excs.append((w.plugin.Name, w.setting.Name, exc))
        self.settingExcDialog(excs, 'Exceptions During Set Default')
        self.enableApplyReset(True, True)

    def settingsReset(self):
        """ set each setting to its previous value

        @return None
        """
        excs = []
        for w in self.queryList('SettingWidget'):
            try:
                w.reset()
            except (Exception, ), exc:
                excs.append((w.plugin.Name, w.setting.Name, exc))
        self.settingExcDialog(excs, 'Exceptions During Reset')
        self.enableApplyReset(False, False)
        
    def settingApply(self):
        """ not final

        """
        excs = []
        for w in self.queryList('SettingWidget'):
            try:
                w.setFromValue()
            except (Exception, ), exc:
                excs.append((w.plugin.Name, w.setting.Name, exc))
        self.settingExcDialog(excs, 'Exceptions During Apply')
        self.enableApplyReset(False, False)
        self.emit(Signals.statusMessage, ('Saving settings...', ))
        self.emit(Signals.berylSettingChanged, ())

    def settingExcDialog(self, exceptions, caption):
        """ show an error dialog

        @param exceptions list of three-tuples
        @param caption dialog caption
        @return None
        """
        if not exceptions:
            return
        excstrs = QStringList()
        for exc in exceptions:
            excstrs.append('Plugin:%s Setting:%s Exception:%s' % exc)
        KMessageBox.errorList(None, caption, excstrs)


class SinglePage(Frame, SettingPage):
    """ SinglePage -> frame to display a single group of settings

    """
    def __init__(self, parent):
        Frame.__init__(self, parent, marginHint, spacingHint)
        self.header = ContentHeader(self)
        self.main = MultiSettingFrame(self)
        self.footer = ContentFooter(self)
        self.buildConnections()

    def buildConnections(self):
        """ build the connections for this instance

        @return None
        """
        SettingPage.buildConnections(self)
        self.connect(self.main.container, Signals.someChange,
                     self.onSomethingChanged)        

    def showSettings(self, plugin, arg):
        """ displays the settings page with individual setting frames

        @param plugin berylsettings Plugin instance
        @param arg setting name or setting section name
        @return None
        """
        if not self.unsavedCheck(plugin, arg):
            return
        self.header.setFromPlugin(plugin, arg)
        self.main.setPlugin(plugin, arg)


class TabPage(Frame, SettingPage):
    """  TabPage -> tab widget to display multiple groups of settings

    """
    def __init__(self, parent):
        Frame.__init__(self, parent, marginHint, spacingHint)
        self.header = ContentHeader(self)
        self.tabs = KTabWidget(self)
        sep = KSeparator(KSeparator.Horizontal, self)
        self.footer = ContentFooter(self)
        self.buildConnections()

    def setGroups(self, plugin):
        """ builds new page for each plugin setting group

        @param plugin berylsettings Plugin instance
        @return None
        """
        if not self.unsavedCheck(plugin, None):
            return
        self.header.setFromPlugin(plugin)
        self.clearPages()
        tabs = self.tabs
        changed = Signals.someChange        
        for group in plugin.groups:
            page = MultiSettingFrame(tabs)
            page.setPlugin(plugin, group.settings(plugin))
            tabs.addTab(page, i18n(group.label))
            self.connect(page.container, changed, self.onSomethingChanged)
        tabs.setCurrentPage(0)

    def clearPages(self):
        """ deletes all pages from the tab widget child

        @return None
        """
        tabs = self.tabs
        page = tabs.currentPage()
        while page:
            tabs.removePage(page)
            page.deleteLater()
            page = tabs.currentPage()            


class MultiSettingFrame(Frame):
    """ MultiSettingFrame -> a scrollable view of multiple settings

    """
    def __init__(self, parent, *args, **kwds):
        Frame.__init__(self, parent)
        self.frames = []
        self.previous = (None, None)        
        self.scroll = ContentScroll(self)
        viewport = self.scroll.viewport()
        self.container = Frame(viewport, marginHint, spacingHint, False)
        self.scroll.addChild(self.container)
        
    def setPlugin(self, plugin, arg):
        """ displays the settings page with individual setting frames

        @param plugin berylsettings Plugin instance
        @param arg setting name, setting type name, or sequence of settings
        @return None
        """
        if isinstance(arg, basestring):
            settings = plugin.settings[arg]
        elif isinstance(arg, Setting):
            settings = [arg, ]
        elif isinstance(arg, (list, tuple)):
            settings = arg
        else:
            print 'unknown setting type', arg
            settings = ()
        if self.previous == (plugin, settings):
            return
        self.previous = (plugin, settings)
        self.clearFrames()
        container = self.container
        layout = container.layout()
        changed = Signals.someChange
        frames = self.frames
        for setting in settings:
            frame = SettingFrame(container, plugin, setting)
            frame.show()
            frames.append(frame)            
            layout.addWidget(frame, 0, Qt.AlignTop)
            self.connect(frame, changed, container, changed)
        layout.addStretch(1)

    def clearFrames(self):
        """ removes child frames and clears the container frame

        @return None
        """
        for widget in self.frames:
           widget.close(True)
        self.container.layout().deleteAllItems()
        self.frames = []


class SettingFrame(QFrame):
    """ SettingFrame -> display and editing widgets for a single Setting.

    """
    def __init__(self, parent, plugin, setting):
        QFrame.__init__(self, parent)
        mainLayout = QVBoxLayout(self)
        innerLayout = QHBoxLayout(mainLayout)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.infoCaption = setting.ShortDesc
        self.infoText = setting.LongDesc
        self.infoButton = info = SmallPushButton(iconSet('help'), '', self)
        self.resetButton = reset = \
            SmallPushButton(iconSet('locationbar_erase'), '', self)
        QToolTip.add(reset, i18n('Reset to default value'))
        self.settingWidget = widget = settingWidget(self, plugin, setting)
        if isinstance(widget.layout(), QHBoxLayout):
            align = Qt.AlignLeft | Qt.AlignVCenter
        else:
            align = Qt.AlignLeft | Qt.AlignTop
        innerLayout.addWidget(info, 0, align)
        innerLayout.addWidget(reset, 0, align)
        innerLayout.addWidget(widget, 1)
        mainLayout.addSpacing(spacingHint)

        connect = self.connect
        connect(info, Signals.clicked, self.showTip)
        connect(reset, Signals.clicked, self.settingWidget.setDefault)
        connect(widget, Signals.someChange, self, Signals.someChange)

    def showTip(self):
        """ show a balloon tip with the setting description

        @return None
        """
        parent = self.resetButton
        pop = KPassivePopup.message(KPassivePopup.Balloon,
                                    self.infoCaption,
                                    self.infoText,
                                    icon('help', size=KIcon.SizeLarge),
                                    parent)
        pos = pop.pos()
        pos.setY(pos.y() + (parent.height() / 2))
        pos.setX(pos.x() + (parent.width() * 2))
        pop.show()
        pop.move(pos)        


class ContentHeader(QFrame):
    """ ContentHeader -> header frame with a pixmap and label for plugin

    """
    def __init__(self, parent):
        QFrame.__init__(self, parent)
        layout = QHBoxLayout(self)
        self.pluginIconLabel = iconLabel = QLabel(self)
        self.pluginNameLabel = nameLabel = QLabel(self)
        layout.addWidget(iconLabel)
        layout.addWidget(nameLabel, 1)

    def setFromPlugin(self, plugin, extra=''):
        """ sets this header icon and label to plugin values

        @param plugin berylsettings Plugin instance
        @param extra any object; if string or unicode, added to end of label
        @return None
        """
        ico = plugin.icon(KIcon.SizeLarge, iconLoader())
        self.pluginIconLabel.setPixmap(ico)
        if isinstance(extra, basestring) and extra:
            extra = ' - %s' % (extra, )
        else:
            extra = ''
        values = (plugin.ShortDesc, extra)
        self.pluginNameLabel.setText('<b>%s%s</b>' % values)


class ContentFooter(QFrame):
    """ ContentFooter -> footer frame with common buttons

    """
    def __init__(self, parent):
        QFrame.__init__(self, parent)
        layout = QHBoxLayout(self)
        self.helpButton = guiButton(self, 'help', layout)
        self.defaultsButton = guiButton(self, 'defaults', layout)
        layout.addStretch(1)
        self.applyButton = guiButton(self, 'apply', layout, False)
        self.resetButton = guiButton(self, 'reset', layout, False)


class ContentScroll(QScrollView):
    """ ContentScroll -> a scroll view fitted to a single child

    The redefined sizeHint method is important -- without it, the
    widget size isn't picked up correctly by the parent.  Found in
    kcontrol sources.
    """
    def __init__(self, parent):
        QScrollView.__init__(self, parent)
        self.setResizePolicy(QScrollView.AutoOneFit)
        self.setFrameShape(self.NoFrame)

    def sizeHint(self):
        """ recommended size for this widget

        @return QSize instance
        """
        return self.minimumSizeHint()