1"""
2@package gui_core.widgets
3
4@brief Core GUI widgets
5
6Classes:
7 - widgets::GNotebook
8 - widgets::ScrolledPanel
9 - widgets::NumTextCtrl
10 - widgets::FloatSlider
11 - widgets::SymbolButton
12 - widgets::StaticWrapText
13 - widgets::BaseValidator
14 - widgets::CoordinatesValidator
15 - widgets::IntegerValidator
16 - widgets::FloatValidator
17 - widgets::EmailValidator
18 - widgets::TimeISOValidator
19 - widgets::MapValidator
20 - widgets::NTCValidator
21 - widgets::SimpleValidator
22 - widgets::GenericValidator
23 - widgets::GListCtrl
24 - widgets::SearchModuleWidget
25 - widgets::ManageSettingsWidget
26 - widgets::PictureComboBox
27 - widgets::ColorTablesComboBox
28 - widgets::BarscalesComboBox
29 - widgets::NArrowsComboBox
30 - widgets::LayersList
31
32@todo:
33 - move validators to a separate file gui_core/validators.py
34
35(C) 2008-2014 by the GRASS Development Team
36
37This program is free software under the GNU General Public License
38(>=v2). Read the file COPYING that comes with GRASS for details.
39
40@author Martin Landa <landa.martin gmail.com> (Google SoC 2008/2010)
41@author Enhancements by Michael Barton <michael.barton asu.edu>
42@author Anna Kratochvilova <kratochanna gmail.com> (Google SoC 2011)
43@author Stepan Turek <stepan.turek seznam.cz> (ManageSettingsWidget - created from GdalSelect)
44@author Matej Krejci <matejkrejci gmail.com> (Google GSoC 2014; EmailValidator, TimeISOValidator)
45"""
46
47import os
48import sys
49import string
50import re
51import six
52from bisect import bisect
53from datetime import datetime
54from core.globalvar import wxPythonPhoenix
55
56import wx
57import wx.lib.mixins.listctrl as listmix
58import wx.lib.scrolledpanel as SP
59from wx.lib.stattext import GenStaticText
60from wx.lib.wordwrap import wordwrap
61if wxPythonPhoenix:
62    import wx.adv
63    from wx.adv import OwnerDrawnComboBox
64else:
65    import wx.combo
66    from wx.combo import OwnerDrawnComboBox
67try:
68    import wx.lib.agw.flatnotebook as FN
69except ImportError:
70    import wx.lib.flatnotebook as FN
71try:
72    from wx.lib.buttons import ThemedGenBitmapTextButton as BitmapTextButton
73except ImportError:  # not sure about TGBTButton version
74    from wx.lib.buttons import GenBitmapTextButton as BitmapTextButton
75try:
76    import wx.lib.agw.customtreectrl as CT
77except ImportError:
78    import wx.lib.customtreectrl as CT
79
80if wxPythonPhoenix:
81    from wx import Validator as Validator
82else:
83    from wx import PyValidator as Validator
84
85from grass.script import core as grass
86
87from grass.pydispatch.signal import Signal
88
89from core import globalvar
90from core.gcmd import GMessage, GError
91from core.debug import Debug
92from gui_core.wrap import Button, SearchCtrl, StaticText, StaticBox, \
93    TextCtrl, Menu, Rect, EmptyBitmap, ListCtrl, NewId, CheckListCtrlMixin
94
95
96class NotebookController:
97    """Provides handling of notebook page names.
98
99    Translates page names to page indices.
100    Class is aggregated in notebook subclasses.
101    Notebook subclasses must delegate methods to controller.
102    Methods inherited from notebook class must be delegated explicitly
103    and other methods can be delegated by @c __getattr__.
104    """
105
106    def __init__(self, classObject, widget):
107        """
108        :param classObject: notebook class name (object, i.e. FlatNotebook)
109        :param widget: notebook instance
110        """
111        self.notebookPages = {}
112        self.classObject = classObject
113        self.widget = widget
114        self.highlightedTextEnd = _(" (...)")
115        self.BindPageChanged()
116
117    def BindPageChanged(self):
118        """Binds page changed event."""
119        self.widget.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnRemoveHighlight)
120
121    def AddPage(self, **kwargs):
122        """Add a new page
123        """
124        if 'name' in kwargs:
125            self.notebookPages[kwargs['name']] = kwargs['page']
126            del kwargs['name']
127
128        self.classObject.AddPage(self.widget, **kwargs)
129
130    def InsertPage(self, **kwargs):
131        """Insert a new page
132        """
133        if 'name' in kwargs:
134            self.notebookPages[kwargs['name']] = kwargs['page']
135            del kwargs['name']
136
137        try:
138            self.classObject.InsertPage(self.widget, **kwargs)
139        except TypeError as e:  # documentation says 'index', but certain versions of wx require 'n'
140            kwargs['n'] = kwargs['index']
141            del kwargs['index']
142            self.classObject.InsertPage(self.widget, **kwargs)
143
144    def DeletePage(self, page):
145        """Delete page
146
147        :param page: name
148        :return: True if page was deleted, False if not exists
149        """
150        delPageIndex = self.GetPageIndexByName(page)
151        if delPageIndex != -1:
152            ret = self.classObject.DeletePage(self.widget, delPageIndex)
153            if ret:
154                del self.notebookPages[page]
155            return ret
156        else:
157            return False
158
159    def RemovePage(self, page):
160        """Delete page without deleting the associated window.
161
162        :param page: name
163        :return: True if page was deleted, False if not exists
164        """
165        delPageIndex = self.GetPageIndexByName(page)
166        if delPageIndex != -1:
167            ret = self.classObject.RemovePage(self.widget, delPageIndex)
168            if ret:
169                del self.notebookPages[page]
170            return ret
171        else:
172            return False
173
174    def SetSelectionByName(self, page):
175        """Set active notebook page.
176
177        :param page: name, eg. 'layers', 'output', 'search', 'pyshell', 'nviz'
178                     (depends on concrete notebook instance)
179        """
180        idx = self.GetPageIndexByName(page)
181        if self.classObject.GetSelection(self.widget) != idx:
182            self.classObject.SetSelection(self.widget, idx)
183
184            self.RemoveHighlight(idx)
185
186    def OnRemoveHighlight(self, event):
187        """Highlighted tab name should be removed."""
188        page = event.GetSelection()
189        self.RemoveHighlight(page)
190        event.Skip()
191
192    def RemoveHighlight(self, page):
193        """Removes highlight string from notebook tab name if necessary.
194
195        :param page: index
196        """
197        text = self.classObject.GetPageText(self.widget, page)
198        if text.endswith(self.highlightedTextEnd):
199            text = text.replace(self.highlightedTextEnd, '')
200            self.classObject.SetPageText(self.widget, page, text)
201
202    def GetPageIndexByName(self, page):
203        """Get notebook page index
204
205        :param page: name
206        """
207        if page not in self.notebookPages:
208            return -1
209        for pageIndex in range(self.classObject.GetPageCount(self.widget)):
210            if self.notebookPages[page] == self.classObject.GetPage(
211                    self.widget, pageIndex):
212                break
213        return pageIndex
214
215    def HighlightPageByName(self, page):
216        pageIndex = self.GetPageIndexByName(page)
217        self.HighlightPage(pageIndex)
218
219    def HighlightPage(self, index):
220        if self.classObject.GetSelection(self.widget) != index:
221            text = self.classObject.GetPageText(self.widget, index)
222            if not text.endswith(self.highlightedTextEnd):
223                text += self.highlightedTextEnd
224            self.classObject.SetPageText(self.widget, index, text)
225
226    def SetPageImage(self, page, index):
227        """Sets image index for page
228
229        :param page: page name
230        :param index: image index (in wx.ImageList)
231        """
232        pageIndex = self.GetPageIndexByName(page)
233        self.classObject.SetPageImage(self.widget, pageIndex, index)
234
235
236class FlatNotebookController(NotebookController):
237    """Controller specialized for FN.FlatNotebook subclasses"""
238
239    def __init__(self, classObject, widget):
240        NotebookController.__init__(self, classObject, widget)
241
242    def BindPageChanged(self):
243        self.widget.Bind(
244            FN.EVT_FLATNOTEBOOK_PAGE_CHANGED,
245            self.OnRemoveHighlight)
246
247    def GetPageIndexByName(self, page):
248        """Get notebook page index
249
250        :param page: name
251        """
252        if page not in self.notebookPages:
253            return -1
254
255        return self.classObject.GetPageIndex(
256            self.widget, self.notebookPages[page])
257
258    def InsertPage(self, **kwargs):
259        """Insert a new page
260        """
261        if 'name' in kwargs:
262            self.notebookPages[kwargs['name']] = kwargs['page']
263            del kwargs['name']
264
265        kwargs['indx'] = kwargs['index']
266        del kwargs['index']
267        self.classObject.InsertPage(self.widget, **kwargs)
268
269
270class GNotebook(FN.FlatNotebook):
271    """Generic notebook widget.
272
273    Enables advanced style settings.
274    Problems with hidden tabs and does not respect system colors (native look).
275    """
276
277    def __init__(self, parent, style, **kwargs):
278        if globalvar.hasAgw:
279            FN.FlatNotebook.__init__(self, parent, id=wx.ID_ANY,
280                                     agwStyle=style, **kwargs)
281        else:
282            FN.FlatNotebook.__init__(self, parent, id=wx.ID_ANY,
283                                     style=style, **kwargs)
284
285        self.controller = FlatNotebookController(classObject=FN.FlatNotebook,
286                                                 widget=self)
287
288    def AddPage(self, **kwargs):
289        """@copydoc NotebookController::AddPage()"""
290        self.controller.AddPage(**kwargs)
291
292    def InsertNBPage(self, **kwargs):
293        """@copydoc NotebookController::InsertPage()"""
294        self.controller.InsertPage(**kwargs)
295
296    def DeleteNBPage(self, page):
297        """@copydoc NotebookController::DeletePage()"""
298        return self.controller.DeletePage(page)
299
300    def RemoveNBPage(self, page):
301        """@copydoc NotebookController::RemovePage()"""
302        return self.controller.RemovePage(page)
303
304    def SetPageImage(self, page, index):
305        """Does nothing because we don't want images for this style"""
306        pass
307
308    def __getattr__(self, name):
309        return getattr(self.controller, name)
310
311
312class FormNotebook(wx.Notebook):
313    """Notebook widget.
314
315    Respects native look.
316    """
317
318    def __init__(self, parent, style):
319        wx.Notebook.__init__(self, parent, id=wx.ID_ANY, style=style)
320        self.controller = NotebookController(classObject=wx.Notebook,
321                                             widget=self)
322
323    def AddPage(self, **kwargs):
324        """@copydoc NotebookController::AddPage()"""
325        self.controller.AddPage(**kwargs)
326
327    def InsertNBPage(self, **kwargs):
328        """@copydoc NotebookController::InsertPage()"""
329        self.controller.InsertPage(**kwargs)
330
331    def DeleteNBPage(self, page):
332        """@copydoc NotebookController::DeletePage()"""
333        return self.controller.DeletePage(page)
334
335    def RemoveNBPage(self, page):
336        """@copydoc NotebookController::RemovePage()"""
337        return self.controller.RemovePage(page)
338
339    def SetPageImage(self, page, index):
340        """@copydoc NotebookController::SetPageImage()"""
341        return self.controller.SetPageImage(page, index)
342
343    def __getattr__(self, name):
344        return getattr(self.controller, name)
345
346
347class FormListbook(wx.Listbook):
348    """Notebook widget.
349
350    Respects native look.
351    """
352
353    def __init__(self, parent, style):
354        wx.Listbook.__init__(self, parent, id=wx.ID_ANY, style=style)
355        self.controller = NotebookController(classObject=wx.Listbook,
356                                             widget=self)
357
358    def AddPage(self, **kwargs):
359        """@copydoc NotebookController::AddPage()"""
360        self.controller.AddPage(**kwargs)
361
362    def InsertPage_(self, **kwargs):
363        """@copydoc NotebookController::InsertPage()"""
364        self.controller.InsertPage(**kwargs)
365
366    def DeletePage(self, page):
367        """@copydoc NotebookController::DeletePage()"""
368        return self.controller.DeletePage(page)
369
370    def RemovePage(self, page):
371        """@copydoc NotebookController::RemovePage()"""
372        return self.controller.RemovePage(page)
373
374    def SetPageImage(self, page, index):
375        """@copydoc NotebookController::SetPageImage()"""
376        return self.controller.SetPageImage(page, index)
377
378    def __getattr__(self, name):
379        return getattr(self.controller, name)
380
381
382class ScrolledPanel(SP.ScrolledPanel):
383    """Custom ScrolledPanel to avoid strange behaviour concerning focus"""
384
385    def __init__(self, parent, style=wx.TAB_TRAVERSAL):
386        SP.ScrolledPanel.__init__(self, parent=parent, id=wx.ID_ANY,
387                                  style=style)
388
389    def OnChildFocus(self, event):
390        pass
391
392
393class NumTextCtrl(TextCtrl):
394    """Class derived from wx.TextCtrl for numerical values only"""
395
396    def __init__(self, parent, **kwargs):
397        ##        self.precision = kwargs.pop('prec')
398        TextCtrl.__init__(self, parent=parent,
399                          validator=NTCValidator(flag='DIGIT_ONLY'),
400                          **kwargs)
401
402    def SetValue(self, value):
403        super(NumTextCtrl, self).SetValue(str(value))
404
405    def GetValue(self):
406        val = super(NumTextCtrl, self).GetValue()
407        if val == '':
408            val = '0'
409        try:
410            return float(val)
411        except ValueError:
412            val = ''.join(''.join(val.split('-')).split('.'))
413            return float(val)
414
415    def SetRange(self, min, max):
416        pass
417
418
419class FloatSlider(wx.Slider):
420    """Class derived from wx.Slider for floats"""
421
422    def __init__(self, **kwargs):
423        Debug.msg(1, "FloatSlider.__init__()")
424        wx.Slider.__init__(self, **kwargs)
425        self.coef = 1.
426        # init range
427        self.minValueOrig = 0
428        self.maxValueOrig = 1
429
430    def SetValue(self, value):
431        value *= self.coef
432        if abs(value) < 1 and value != 0:
433            while abs(value) < 1:
434                value *= 100
435                self.coef *= 100
436            super(FloatSlider, self).SetRange(self.minValueOrig * self.coef,
437                                              self.maxValueOrig * self.coef)
438        super(FloatSlider, self).SetValue(value)
439
440        Debug.msg(4, "FloatSlider.SetValue(): value = %f" % value)
441
442    def SetRange(self, minValue, maxValue):
443        self.coef = 1.
444        self.minValueOrig = minValue
445        self.maxValueOrig = maxValue
446        if abs(minValue) < 1 or abs(maxValue) < 1:
447            while (abs(minValue) < 1 and minValue != 0) or (
448                    abs(maxValue) < 1 and maxValue != 0):
449                minValue *= 100
450                maxValue *= 100
451                self.coef *= 100
452            super(
453                FloatSlider,
454                self).SetValue(
455                super(
456                    FloatSlider,
457                    self).GetValue() *
458                self.coef)
459        super(FloatSlider, self).SetRange(minValue, maxValue)
460        Debug.msg(
461            4, "FloatSlider.SetRange(): minValue = %f, maxValue = %f" %
462            (minValue, maxValue))
463
464    def GetValue(self):
465        val = super(FloatSlider, self).GetValue()
466        Debug.msg(4, "FloatSlider.GetValue(): value = %f" % (val / self.coef))
467        return val / self.coef
468
469
470class SymbolButton(BitmapTextButton):
471    """Button with symbol and label."""
472
473    def __init__(self, parent, usage, label, **kwargs):
474        """Constructor
475
476        :param parent: parent (usually wx.Panel)
477        :param usage: determines usage and picture
478        :param label: displayed label
479        """
480        size = (15, 15)
481        buffer = EmptyBitmap(*size)
482        BitmapTextButton.__init__(self, parent=parent, label=" " + label,
483                                  bitmap=buffer, **kwargs)
484
485        dc = wx.MemoryDC()
486        dc.SelectObject(buffer)
487        maskColor = wx.Colour(255, 255, 255)
488        dc.SetBrush(wx.Brush(maskColor))
489        dc.Clear()
490
491        if usage == 'record':
492            self.DrawRecord(dc, size)
493        elif usage == 'stop':
494            self.DrawStop(dc, size)
495        elif usage == 'play':
496            self.DrawPlay(dc, size)
497        elif usage == 'pause':
498            self.DrawPause(dc, size)
499
500        if sys.platform not in ("win32", "darwin"):
501            buffer.SetMaskColour(maskColor)
502        self.SetBitmapLabel(buffer)
503        dc.SelectObject(wx.NullBitmap)
504
505    def DrawRecord(self, dc, size):
506        """Draw record symbol"""
507        dc.SetBrush(wx.Brush(wx.Colour(255, 0, 0)))
508        dc.DrawCircle(size[0] / 2, size[1] / 2, size[0] / 2)
509
510    def DrawStop(self, dc, size):
511        """Draw stop symbol"""
512        dc.SetBrush(wx.Brush(wx.Colour(50, 50, 50)))
513        dc.DrawRectangle(0, 0, size[0], size[1])
514
515    def DrawPlay(self, dc, size):
516        """Draw play symbol"""
517        dc.SetBrush(wx.Brush(wx.Colour(0, 255, 0)))
518        points = (wx.Point(0, 0), wx.Point(0, size[1]), wx.Point(size[0],
519                                                                 size[1] / 2))
520        dc.DrawPolygon(points)
521
522    def DrawPause(self, dc, size):
523        """Draw pause symbol"""
524        dc.SetBrush(wx.Brush(wx.Colour(50, 50, 50)))
525        dc.DrawRectangle(0, 0, 2 * size[0] / 5, size[1])
526        dc.DrawRectangle(3 * size[0] / 5, 0, 2 * size[0] / 5, size[1])
527
528
529class StaticWrapText(GenStaticText):
530    """A Static Text widget that wraps its text to fit parents width,
531    enlarging its height if necessary."""
532
533    def __init__(self, parent, id=wx.ID_ANY,
534                 label='', margin=0, *args, **kwds):
535        self._margin = margin
536        self._initialLabel = label
537        self.init = False
538        GenStaticText.__init__(self, parent, id, label, *args, **kwds)
539        self.Bind(wx.EVT_SIZE, self.OnSize)
540
541    def DoGetBestSize(self):
542        """Overriden method which reports widget's best size."""
543        if not self.init:
544            self.init = True
545            self._updateLabel()
546
547        parent = self.GetParent()
548        newExtent = wx.ClientDC(parent).GetMultiLineTextExtent(self.GetLabel())
549        # when starting, width is very small and height is big which creates
550        # very high windows
551        if newExtent[0] < newExtent[1]:
552            return (0, 0)
553        return newExtent[:2]
554
555    def OnSize(self, event):
556        self._updateLabel()
557        event.Skip()
558
559    def _updateLabel(self):
560        """Calculates size of wrapped label"""
561        parent = self.GetParent()
562        newLabel = wordwrap(text=self._initialLabel, width=parent.GetSize()[0],
563                            dc=wx.ClientDC(parent), breakLongWords=True,
564                            margin=self._margin)
565        GenStaticText.SetLabel(self, newLabel)
566
567    def SetLabel(self, label):
568        self._initialLabel = label
569        self._updateLabel()
570
571
572class BaseValidator(Validator):
573
574    def __init__(self):
575        Validator.__init__(self)
576
577        self.Bind(wx.EVT_TEXT, self.OnText)
578
579    def OnText(self, event):
580        """Do validation"""
581        self._validate(win=event.GetEventObject())
582
583        event.Skip()
584
585    def Validate(self, parent):
586        """Is called upon closing wx.Dialog"""
587        win = self.GetWindow()
588        return self._validate(win)
589
590    def _validate(self, win):
591        """Validate input"""
592        text = win.GetValue()
593
594        if text:
595            try:
596                self.type(text)
597            except ValueError:
598                self._notvalid()
599                return False
600
601        self._valid()
602        return True
603
604    def _notvalid(self):
605        textCtrl = self.GetWindow()
606
607        textCtrl.SetBackgroundColour("grey")
608        textCtrl.Refresh()
609
610    def _valid(self):
611        textCtrl = self.GetWindow()
612
613        sysColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
614        textCtrl.SetBackgroundColour(sysColor)
615
616        textCtrl.Refresh()
617        return True
618
619    def TransferToWindow(self):
620        return True  # Prevent wxDialog from complaining.
621
622    def TransferFromWindow(self):
623        return True  # Prevent wxDialog from complaining.
624
625
626class CoordinatesValidator(BaseValidator):
627    """Validator for coordinates input (list of floats separated by comma)"""
628
629    def __init__(self):
630        BaseValidator.__init__(self)
631
632    def _validate(self, win):
633        """Validate input"""
634        text = win.GetValue()
635        if text:
636            try:
637                text = text.split(',')
638
639                for t in text:
640                    float(t)
641
642                if len(text) % 2 != 0:
643                    return False
644
645            except ValueError:
646                self._notvalid()
647                return False
648
649        self._valid()
650        return True
651
652    def Clone(self):
653        """Clone validator"""
654        return CoordinatesValidator()
655
656
657class IntegerValidator(BaseValidator):
658    """Validator for floating-point input"""
659
660    def __init__(self):
661        BaseValidator.__init__(self)
662        self.type = int
663
664    def Clone(self):
665        """Clone validator"""
666        return IntegerValidator()
667
668
669class FloatValidator(BaseValidator):
670    """Validator for floating-point input"""
671
672    def __init__(self):
673        BaseValidator.__init__(self)
674        self.type = float
675
676    def Clone(self):
677        """Clone validator"""
678        return FloatValidator()
679
680
681class EmailValidator(BaseValidator):
682    """Validator for email input"""
683
684    def __init__(self):
685        BaseValidator.__init__(self)
686
687    def _validate(self, win):
688        """Validate input"""
689        text = win.GetValue()
690        if text:
691            if re.match(r'\b[\w.-]+@[\w.-]+.\w{2,4}\b', text) is None:
692                self._notvalid()
693                return False
694
695        self._valid()
696        return True
697
698    def Clone(self):
699        """Clone validator"""
700        return EmailValidator()
701
702
703class TimeISOValidator(BaseValidator):
704    """Validator for time ISO format (YYYY-MM-DD) input"""
705
706    def __init__(self):
707        BaseValidator.__init__(self)
708
709    def _validate(self, win):
710        """Validate input"""
711        text = win.GetValue()
712        if text:
713            try:
714                datetime.strptime(text, '%Y-%m-%d')
715            except:
716                self._notvalid()
717                return False
718
719        self._valid()
720        return True
721
722    def Clone(self):
723        """Clone validator"""
724        return TimeISOValidator()
725
726
727class NTCValidator(Validator):
728    """validates input in textctrls, taken from wxpython demo"""
729
730    def __init__(self, flag=None):
731        Validator.__init__(self)
732        self.flag = flag
733        self.Bind(wx.EVT_CHAR, self.OnChar)
734
735    def Clone(self):
736        return NTCValidator(self.flag)
737
738    def OnChar(self, event):
739        key = event.GetKeyCode()
740        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255:
741            event.Skip()
742            return
743        if self.flag == 'DIGIT_ONLY' and chr(key) in string.digits + '.-':
744            event.Skip()
745            return
746        if not wx.Validator_IsSilent():
747            wx.Bell()
748        # Returning without calling even.Skip eats the event before it
749        # gets to the text control
750        return
751
752
753class SimpleValidator(Validator):
754    """This validator is used to ensure that the user has entered something
755        into the text object editor dialog's text field.
756    """
757
758    def __init__(self, callback):
759        """Standard constructor.
760        """
761        Validator.__init__(self)
762        self.callback = callback
763
764    def Clone(self):
765        """Standard cloner.
766
767        Note that every validator must implement the Clone() method.
768        """
769        return SimpleValidator(self.callback)
770
771    def Validate(self, win):
772        """Validate the contents of the given text control.
773        """
774        ctrl = self.GetWindow()
775        text = ctrl.GetValue()
776        if len(text) == 0:
777            self.callback(ctrl)
778            return False
779        else:
780            return True
781
782    def TransferToWindow(self):
783        """Transfer data from validator to window.
784
785        The default implementation returns False, indicating that an
786        error occurred.  We simply return True, as we don't do any data
787        transfer.
788        """
789        return True  # Prevent wxDialog from complaining.
790
791    def TransferFromWindow(self):
792        """Transfer data from window to validator.
793
794        The default implementation returns False, indicating that an
795        error occurred.  We simply return True, as we don't do any data
796        transfer.
797        """
798        return True  # Prevent wxDialog from complaining.
799
800
801class GenericValidator(Validator):
802    """This validator checks condition and calls callback
803    in case the condition is not fulfilled.
804    """
805
806    def __init__(self, condition, callback):
807        """Standard constructor.
808
809        :param condition: function which accepts string value and returns T/F
810        :param callback: function which is called when condition is not fulfilled
811        """
812        Validator.__init__(self)
813        self._condition = condition
814        self._callback = callback
815
816    def Clone(self):
817        """Standard cloner.
818
819        Note that every validator must implement the Clone() method.
820        """
821        return GenericValidator(self._condition, self._callback)
822
823    def Validate(self, win):
824        """Validate the contents of the given text control.
825        """
826        ctrl = self.GetWindow()
827        text = ctrl.GetValue()
828        if not self._condition(text):
829            self._callback(ctrl)
830            return False
831        else:
832            return True
833
834    def TransferToWindow(self):
835        """Transfer data from validator to window.
836        """
837        return True  # Prevent wxDialog from complaining.
838
839    def TransferFromWindow(self):
840        """Transfer data from window to validator.
841        """
842        return True  # Prevent wxDialog from complaining.
843
844
845class MapValidator(GenericValidator):
846    """Validator for map name input
847
848    See G_legal_filename()
849    """
850
851    def __init__(self):
852        def _mapNameValidationFailed(ctrl):
853            message = _(
854                "Name <%(name)s> is not a valid name for GRASS map. "
855                "Please use only ASCII characters excluding %(chars)s "
856                "and space.") % {
857                'name': ctrl.GetValue(),
858                'chars': '/"\'@,=*~'}
859            GError(message, caption=_("Invalid name"))
860
861        GenericValidator.__init__(self,
862                                  grass.legal_name,
863                                  _mapNameValidationFailed)
864
865
866class SingleSymbolPanel(wx.Panel):
867    """Panel for displaying one symbol.
868
869    Changes background when selected. Assumes that parent will catch
870    events emitted on mouse click. Used in gui_core::dialog::SymbolDialog.
871    """
872
873    def __init__(self, parent, symbolPath):
874        """Panel constructor
875
876        Signal symbolSelectionChanged - symbol selected
877                                      - attribute 'name' (symbol name)
878                                      - attribute 'doubleClick' (underlying cause)
879
880        :param parent: parent (gui_core::dialog::SymbolDialog)
881        :param symbolPath: absolute path to symbol
882        """
883        self.symbolSelectionChanged = Signal(
884            'SingleSymbolPanel.symbolSelectionChanged')
885
886        wx.Panel.__init__(self, parent, id=wx.ID_ANY, style=wx.BORDER_RAISED)
887        self.SetName(os.path.splitext(os.path.basename(symbolPath))[0])
888        self.sBmp = wx.StaticBitmap(self, wx.ID_ANY, wx.Bitmap(symbolPath))
889
890        self.selected = False
891        self.selectColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)
892        self.deselectColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
893
894        sizer = wx.BoxSizer()
895        sizer.Add(
896            self.sBmp,
897            proportion=0,
898            flag=wx.ALL | wx.ALIGN_CENTER,
899            border=5)
900        self.SetBackgroundColour(self.deselectColor)
901        self.SetMinSize(self.GetBestSize())
902        self.SetSizerAndFit(sizer)
903
904        # binding to both (staticBitmap, Panel) necessary
905        self.sBmp.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
906        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
907        self.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick)
908        self.sBmp.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick)
909
910    def OnLeftDown(self, event):
911        """Panel selected, background changes"""
912        self.selected = True
913        self.SetBackgroundColour(self.selectColor)
914        self.Refresh()
915        event.Skip()
916
917        self.symbolSelectionChanged.emit(
918            name=self.GetName(), doubleClick=False)
919
920    def OnDoubleClick(self, event):
921        self.symbolSelectionChanged.emit(name=self.GetName(), doubleClick=True)
922
923    def Deselect(self):
924        """Panel deselected, background changes back to default"""
925        self.selected = False
926        self.SetBackgroundColour(self.deselectColor)
927        self.Refresh()
928
929    def Select(self):
930        """Select panel, no event emitted"""
931        self.selected = True
932        self.SetBackgroundColour(self.selectColor)
933        self.Refresh()
934
935
936class GListCtrl(ListCtrl, listmix.ListCtrlAutoWidthMixin,
937                CheckListCtrlMixin):
938    """Generic ListCtrl with popup menu to select/deselect all
939    items"""
940
941    def __init__(self, parent):
942        self.parent = parent
943
944        ListCtrl.__init__(self, parent, id=wx.ID_ANY,
945                             style=wx.LC_REPORT)
946        CheckListCtrlMixin.__init__(self)
947
948        # setup mixins
949        listmix.ListCtrlAutoWidthMixin.__init__(self)
950
951        self.Bind(wx.EVT_COMMAND_RIGHT_CLICK, self.OnPopupMenu)  # wxMSW
952        self.Bind(wx.EVT_RIGHT_UP, self.OnPopupMenu)  # wxGTK
953
954    def OnPopupMenu(self, event):
955        """Show popup menu"""
956        if self.GetItemCount() < 1:
957            return
958
959        if not hasattr(self, "popupDataID1"):
960            self.popupDataID1 = NewId()
961            self.popupDataID2 = NewId()
962
963            self.Bind(wx.EVT_MENU, self.OnSelectAll, id=self.popupDataID1)
964            self.Bind(wx.EVT_MENU, self.OnSelectNone, id=self.popupDataID2)
965
966        # generate popup-menu
967        menu = Menu()
968        menu.Append(self.popupDataID1, _("Select all"))
969        menu.Append(self.popupDataID2, _("Deselect all"))
970
971        self.PopupMenu(menu)
972        menu.Destroy()
973
974    def OnSelectAll(self, event):
975        """Select all items"""
976        item = -1
977
978        while True:
979            item = self.GetNextItem(item)
980            if item == -1:
981                break
982            self.CheckItem(item, True)
983
984        event.Skip()
985
986    def OnSelectNone(self, event):
987        """Deselect items"""
988        item = -1
989
990        while True:
991            item = self.GetNextItem(item, wx.LIST_STATE_SELECTED)
992            if item == -1:
993                break
994            self.CheckItem(item, False)
995
996        event.Skip()
997
998    def GetData(self, checked=None):
999        """Get list data"""
1000        data = []
1001        checkedList = []
1002
1003        item = -1
1004        while True:
1005
1006            row = []
1007            item = self.GetNextItem(item)
1008            if item == -1:
1009                break
1010
1011            isChecked = self.IsItemChecked(item)
1012            if checked is not None and checked != isChecked:
1013                continue
1014
1015            checkedList.append(isChecked)
1016
1017            for i in range(self.GetColumnCount()):
1018                row.append(self.GetItem(item, i).GetText())
1019
1020            row.append(item)
1021            data.append(tuple(row))
1022
1023        if checked is not None:
1024            return tuple(data)
1025        else:
1026            return (tuple(data), tuple(checkedList))
1027
1028    def LoadData(self, data=None, selectOne=True):
1029        """Load data into list"""
1030        self.DeleteAllItems()
1031        if data is None:
1032            return
1033
1034        idx = 0
1035        for item in data:
1036            index = self.InsertItem(idx, str(item[0]))
1037            for i in range(1, self.GetColumnCount()):
1038                self.SetItem(index, i, item[i])
1039            idx += 1
1040
1041        # check by default only on one item
1042        if len(data) == 1 and selectOne:
1043            self.CheckItem(index, True)
1044
1045
1046class SearchModuleWidget(wx.Panel):
1047    """Search module widget (used e.g. in SearchModuleWindow)
1048
1049    Signals:
1050        moduleSelected - attribute 'name' is module name
1051        showSearchResult - attribute 'result' is a node (representing module)
1052        showNotification - attribute 'message'
1053    """
1054
1055    def __init__(self, parent, model,
1056                 showChoice=True, showTip=False, **kwargs):
1057        self._showTip = showTip
1058        self._showChoice = showChoice
1059        self._model = model
1060        self._results = []  # list of found nodes
1061        self._resultIndex = -1
1062        self._searchKeys = ['description', 'keywords', 'command']
1063        self._oldValue = ''
1064
1065        self.moduleSelected = Signal('SearchModuleWidget.moduleSelected')
1066        self.showSearchResult = Signal('SearchModuleWidget.showSearchResult')
1067        self.showNotification = Signal('SearchModuleWidget.showNotification')
1068
1069        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY, **kwargs)
1070
1071#        self._box = wx.StaticBox(parent = self, id = wx.ID_ANY,
1072# label = " %s " % _("Find module - (press Enter for next match)"))
1073
1074        if sys.platform == 'win32':
1075            self._search = TextCtrl(
1076                parent=self, id=wx.ID_ANY, size=(-1, 25),
1077                style=wx.TE_PROCESS_ENTER)
1078        else:
1079            self._search = SearchCtrl(
1080                parent=self, id=wx.ID_ANY, size=(-1, 25),
1081                style=wx.TE_PROCESS_ENTER)
1082            self._search.SetDescriptiveText(_('Fulltext search'))
1083            self._search.SetToolTip(
1084                _("Type to search in all modules. Press Enter for next match."))
1085
1086        self._search.Bind(wx.EVT_TEXT, self.OnSearchModule)
1087        self._search.Bind(wx.EVT_TEXT_ENTER, self.OnEnter)
1088
1089        if self._showTip:
1090            self._searchTip = StaticWrapText(parent=self, id=wx.ID_ANY,
1091                                  label="Choose a module", size=(-1, 35))
1092
1093        if self._showChoice:
1094            self._searchChoice = wx.Choice(parent=self, id=wx.ID_ANY)
1095            self._searchChoice.SetItems(
1096                self._searchModule(
1097                    keys=['command'], value=''))
1098            self._searchChoice.Bind(wx.EVT_CHOICE, self.OnSelectModule)
1099
1100        self._layout()
1101
1102    def _layout(self):
1103        """Do layout"""
1104        sizer = wx.BoxSizer(wx.HORIZONTAL)
1105        boxSizer = wx.BoxSizer(wx.VERTICAL)
1106
1107        boxSizer.Add(self._search,
1108                     flag=wx.EXPAND | wx.BOTTOM,
1109                     border=5)
1110        if self._showChoice:
1111            hSizer = wx.BoxSizer(wx.HORIZONTAL)
1112            hSizer.Add(self._searchChoice,
1113                       flag=wx.EXPAND | wx.BOTTOM,
1114                       border=5)
1115            hSizer.AddStretchSpacer()
1116            boxSizer.Add(hSizer, flag=wx.EXPAND)
1117        if self._showTip:
1118            boxSizer.Add(self._searchTip,
1119                         flag=wx.EXPAND)
1120
1121        sizer.Add(boxSizer, proportion=1)
1122
1123        self.SetSizer(sizer)
1124        sizer.Fit(self)
1125
1126    def OnEnter(self, event):
1127        """Process EVT_TEXT_ENTER to show search results"""
1128        self._showSearchResult()
1129        event.Skip()
1130
1131    def _showSearchResult(self):
1132        if self._results:
1133            self._resultIndex += 1
1134            if self._resultIndex == len(self._results):
1135                self._resultIndex = 0
1136            self.showSearchResult.emit(result=self._results[self._resultIndex])
1137
1138    def OnSearchModule(self, event):
1139        """Search module by keywords or description"""
1140        value = self._search.GetValue()
1141        if value == self._oldValue:
1142            event.Skip()
1143            return
1144        self._oldValue = value
1145
1146        if len(value) <= 2:
1147            if len(value) == 0:  # reset
1148                commands = self._searchModule(keys=['command'], value='')
1149            else:
1150                self.showNotification.emit(
1151                    message=_("Searching, please type more characters."))
1152                return
1153        else:
1154            commands = self._searchModule(keys=self._searchKeys, value=value)
1155        if self._showChoice:
1156            self._searchChoice.SetItems(commands)
1157            if commands:
1158                self._searchChoice.SetSelection(0)
1159
1160        label = _("%d modules match") % len(commands)
1161        if self._showTip:
1162            self._searchTip.SetLabel(label)
1163
1164        self.showNotification.emit(message=label)
1165
1166        event.Skip()
1167
1168    def _searchModule(self, keys, value):
1169        """Search modules by keys
1170
1171        :param keys: list of keys
1172        :param value: patter to match
1173        """
1174        nodes = set()
1175        for key in keys:
1176            nodes.update(self._model.SearchNodes(key=key, value=value))
1177
1178        nodes = list(nodes)
1179        nodes.sort(key=lambda node: self._model.GetIndexOfNode(node))
1180        self._results = nodes
1181        self._resultIndex = -1
1182        commands = sorted([node.data['command']
1183                           for node in nodes if node.data['command']])
1184
1185        return commands
1186
1187    def OnSelectModule(self, event):
1188        """Module selected from choice, update command prompt"""
1189        cmd = self._searchChoice.GetStringSelection()
1190        self.moduleSelected.emit(name=cmd)
1191
1192        if self._showTip:
1193            for module in self._results:
1194                if cmd == module.data['command']:
1195                    self._searchTip.SetLabel(module.data['description'])
1196                    break
1197
1198    def Reset(self):
1199        """Reset widget"""
1200        self._search.SetValue('')
1201        if self._showTip:
1202            self._searchTip.SetLabel('Choose a module')
1203
1204
1205class ManageSettingsWidget(wx.Panel):
1206    """Widget which allows loading and saving settings into file."""
1207
1208    def __init__(self, parent, settingsFile):
1209        """
1210        Signals:
1211            settingsChanged - called when users changes setting
1212                            - attribute 'data' with chosen setting data
1213            settingsSaving - called when settings are saving
1214                           - attribute 'name' with chosen settings name
1215            settingsLoaded - called when settings are loaded
1216                           - attribute 'settings' is dict with loaded settings
1217                             {nameofsetting : settingdata, ....}
1218
1219        :param settingsFile: path to file, where settings will be saved and loaded from
1220        """
1221        self.settingsFile = settingsFile
1222
1223        self.settingsChanged = Signal('ManageSettingsWidget.settingsChanged')
1224        self.settingsSaving = Signal('ManageSettingsWidget.settingsSaving')
1225        self.settingsLoaded = Signal('ManageSettingsWidget.settingsLoaded')
1226
1227        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
1228
1229        self.settingsBox = StaticBox(parent=self, id=wx.ID_ANY,
1230                                     label=" %s " % _("Profiles"))
1231
1232        self.settingsChoice = wx.Choice(parent=self, id=wx.ID_ANY)
1233        self.settingsChoice.Bind(wx.EVT_CHOICE, self.OnSettingsChanged)
1234        self.btnSettingsSave = Button(parent=self, id=wx.ID_SAVE)
1235        self.btnSettingsSave.Bind(wx.EVT_BUTTON, self.OnSettingsSave)
1236        self.btnSettingsSave.SetToolTip(_("Save current settings"))
1237        self.btnSettingsDel = Button(parent=self, id=wx.ID_REMOVE)
1238        self.btnSettingsDel.Bind(wx.EVT_BUTTON, self.OnSettingsDelete)
1239        self.btnSettingsSave.SetToolTip(
1240            _("Delete currently selected settings"))
1241
1242        # escaping with '$' character - index in self.esc_chars
1243        self.e_char_i = 0
1244        self.esc_chars = ['$', ';']
1245
1246        self._settings = self._loadSettings()  # -> self.settingsChoice.SetItems()
1247        self.settingsLoaded.emit(settings=self._settings)
1248
1249        self.data_to_save = []
1250
1251        self._layout()
1252
1253        self.SetSizer(self.settingsSizer)
1254        self.settingsSizer.Fit(self)
1255
1256    def _layout(self):
1257
1258        self.settingsSizer = wx.StaticBoxSizer(self.settingsBox, wx.HORIZONTAL)
1259        self.settingsSizer.Add(
1260            StaticText(
1261                parent=self,
1262                id=wx.ID_ANY,
1263                label=_("Load:")),
1264            flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT,
1265            border=5)
1266        self.settingsSizer.Add(
1267            self.settingsChoice,
1268            proportion=1,
1269            flag=wx.EXPAND | wx.BOTTOM,
1270            border=3)
1271        self.settingsSizer.Add(self.btnSettingsSave,
1272                               flag=wx.LEFT | wx.RIGHT | wx.BOTTOM, border=3)
1273        self.settingsSizer.Add(self.btnSettingsDel,
1274                               flag=wx.RIGHT | wx.BOTTOM, border=3)
1275
1276    def OnSettingsChanged(self, event):
1277        """Load named settings"""
1278        name = event.GetString()
1279        if name not in self._settings:
1280            GError(parent=self,
1281                   message=_("Settings <%s> not found") % name)
1282            return
1283
1284        data = self._settings[name]
1285        self.settingsChanged.emit(data=data)
1286
1287    def GetSettings(self):
1288        """Load named settings"""
1289        return self._settings.copy()
1290
1291    def OnSettingsSave(self, event):
1292        """Save settings"""
1293        dlg = wx.TextEntryDialog(parent=self,
1294                                 message=_("Name:"),
1295                                 caption=_("Save settings"))
1296        if dlg.ShowModal() == wx.ID_OK:
1297            name = dlg.GetValue()
1298            if not name:
1299                GMessage(parent=self,
1300                         message=_("Name not given, settings is not saved."))
1301            else:
1302                self.settingsSaving.emit(name=name)
1303
1304            dlg.Destroy()
1305
1306    def SaveSettings(self, name):
1307        # check if settings item already exists
1308        if name in self._settings:
1309            dlgOwt = wx.MessageDialog(
1310                self,
1311                message=_(
1312                    "Settings <%s> already exists. "
1313                    "Do you want to overwrite the settings?") %
1314                name,
1315                caption=_("Save settings"),
1316                style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
1317            if dlgOwt.ShowModal() != wx.ID_YES:
1318                dlgOwt.Destroy()
1319                return
1320
1321        if self.data_to_save:
1322            self._settings[name] = self.data_to_save
1323
1324        self._saveSettings()
1325        self.settingsChoice.SetStringSelection(name)
1326
1327        self.data_to_save = []
1328
1329    def _saveSettings(self):
1330        """Save settings and reload if successful"""
1331        if self._writeSettings() == 0:
1332            self._settings = self._loadSettings()
1333
1334    def SetDataToSave(self, data):
1335        """Set data for setting, which will be saved.
1336
1337        :param data: - list of strings, which will be saved
1338        """
1339        self.data_to_save = data
1340
1341    def SetSettings(self, settings):
1342        """Set settings
1343
1344        :param settings: - dict with all settigs {nameofsetting : settingdata, ....}
1345        """
1346        self._settings = settings
1347        self._saveSettings()
1348
1349    def AddSettings(self, settings):
1350        """Add settings
1351
1352        :param settings: - dict with all settigs {nameofsetting : settingdata, ....}
1353        """
1354        self._settings.update(settings)
1355        self._saveSettings()
1356
1357    def OnSettingsDelete(self, event):
1358        """Save settings
1359        """
1360        name = self.settingsChoice.GetStringSelection()
1361        if not name:
1362            GMessage(parent=self,
1363                     message=_("No settings is defined. Operation canceled."))
1364            return
1365
1366        self._settings.pop(name)
1367        if self._writeSettings() == 0:
1368            self._settings = self._loadSettings()
1369
1370    def _writeSettings(self):
1371        """Save settings into the file
1372
1373        :return: 0 on success
1374        :return: -1 on failure
1375        """
1376        try:
1377            fd = open(self.settingsFile, 'w')
1378            fd.write('format_version=2.0\n')
1379            for key, values in six.iteritems(self._settings):
1380                first = True
1381                for v in values:
1382                    # escaping characters
1383                    for e_ch in self.esc_chars:
1384                        v = v.replace(
1385                            e_ch, self.esc_chars[
1386                                self.e_char_i] + e_ch)
1387                    if first:
1388                        # escaping characters
1389                        for e_ch in self.esc_chars:
1390                            key = key.replace(
1391                                e_ch, self.esc_chars[
1392                                    self.e_char_i] + e_ch)
1393                        fd.write('%s;%s;' % (key, v))
1394                        first = False
1395                    else:
1396                        fd.write('%s;' % (v))
1397                fd.write('\n')
1398
1399        except IOError:
1400            GError(parent=self,
1401                   message=_("Unable to save settings"))
1402            return -1
1403        fd.close()
1404
1405        return 0
1406
1407    def _loadSettings(self):
1408        """Load settings from the file
1409
1410        The file is defined by self.SettingsFile.
1411
1412        :return: parsed dict
1413        :return: empty dict on error
1414        """
1415
1416        data = dict()
1417        if not os.path.exists(self.settingsFile):
1418            return data
1419
1420        try:
1421            fd = open(self.settingsFile, 'r')
1422        except IOError:
1423            return data
1424
1425        fd_lines = fd.readlines()
1426
1427        if not fd_lines:
1428            fd.close()
1429            return data
1430
1431        if fd_lines[0].strip() == 'format_version=2.0':
1432            data = self._loadSettings_v2(fd_lines)
1433        else:
1434            data = self._loadSettings_v1(fd_lines)
1435
1436        self.settingsChoice.SetItems(sorted(data.keys()))
1437        fd.close()
1438
1439        self.settingsLoaded.emit(settings=data)
1440
1441        return data
1442
1443    def _loadSettings_v2(self, fd_lines):
1444        """Load settings from the file in format version 2.0
1445
1446        The file is defined by self.SettingsFile.
1447
1448        :return: parsed dict
1449        :return: empty dict on error
1450        """
1451        data = dict()
1452
1453        for line in fd_lines[1:]:
1454            try:
1455                lineData = []
1456                line = line.rstrip('\n')
1457                i_last_found = i_last = 0
1458                key = ''
1459                while True:
1460                    idx = line.find(';', i_last)
1461                    if idx < 0:
1462                        break
1463                    elif idx != 0:
1464
1465                        # find out whether it is separator
1466                        # $$$$; - it is separator
1467                        # $$$$$; - it is not separator
1468                        i_esc_chars = 0
1469                        while True:
1470                            if line[idx - (i_esc_chars + 1)
1471                                    ] == self.esc_chars[self.e_char_i]:
1472                                i_esc_chars += 1
1473                            else:
1474                                break
1475                        if i_esc_chars % 2 != 0:
1476                            i_last = idx + 1
1477                            continue
1478
1479                    lineItem = line[i_last_found: idx]
1480                    # unescape characters
1481                    for e_ch in self.esc_chars:
1482                        lineItem = lineItem.replace(
1483                            self.esc_chars[self.e_char_i] + e_ch, e_ch)
1484                    if i_last_found == 0:
1485                        key = lineItem
1486                    else:
1487                        lineData.append(lineItem)
1488                    i_last_found = i_last = idx + 1
1489                if key and lineData:
1490                    data[key] = lineData
1491            except ValueError:
1492                pass
1493
1494        return data
1495
1496    def _loadSettings_v1(self, fd_lines):
1497        """Load settings from the file in format version 1.0 (backward compatibility)
1498
1499        The file is defined by self.SettingsFile.
1500
1501        :return: parsed dict
1502        :return: empty dict on error
1503        """
1504        data = dict()
1505
1506        for line in fd_lines:
1507            try:
1508                lineData = line.rstrip('\n').split(';')
1509                if len(lineData) > 4:
1510                    # type, dsn, format, options
1511                    data[
1512                        lineData[0]] = (
1513                        lineData[1],
1514                        lineData[2],
1515                        lineData[3],
1516                        lineData[4])
1517                else:
1518                    data[
1519                        lineData[0]] = (
1520                        lineData[1],
1521                        lineData[2],
1522                        lineData[3],
1523                        '')
1524            except ValueError:
1525                pass
1526
1527        return data
1528
1529
1530class PictureComboBox(OwnerDrawnComboBox):
1531    """Abstract class of ComboBox with pictures.
1532
1533        Derived class has to specify has to specify _getPath method.
1534    """
1535
1536    def OnDrawItem(self, dc, rect, item, flags):
1537        """Overridden from OwnerDrawnComboBox.
1538
1539        Called to draw each item in the list.
1540        """
1541        if item == wx.NOT_FOUND:
1542            # painting the control, but there is no valid item selected yet
1543            return
1544
1545        r = Rect(*rect)  # make a copy
1546        r.Deflate(3, 5)
1547
1548        # for painting the items in the popup
1549        bitmap = self.GetPictureBitmap(self.GetString(item))
1550        if bitmap:
1551            dc.DrawBitmap(
1552                bitmap, r.x, r.y + (r.height - bitmap.GetHeight()) / 2)
1553            width = bitmap.GetWidth() + 10
1554        else:
1555            width = 0
1556        dc.DrawText(self.GetString(item),
1557                    r.x + width,
1558                    (r.y + 0) + (r.height - dc.GetCharHeight()) / 2)
1559
1560    def OnMeasureItem(self, item):
1561        """Overridden from OwnerDrawnComboBox, should return the height.
1562
1563        Needed to display an item in the popup, or -1 for default.
1564        """
1565        return 24
1566
1567    def GetPictureBitmap(self, name):
1568        """Returns bitmap for given picture name.
1569
1570        :param str colorTable: name of color table
1571        """
1572        if not hasattr(self, 'bitmaps'):
1573            self.bitmaps = {}
1574
1575        if name in self.bitmaps:
1576            return self.bitmaps[name]
1577
1578        path = self._getPath(name)
1579        if os.path.exists(path):
1580            bitmap = wx.Bitmap(path)
1581            self.bitmaps[name] = bitmap
1582            return bitmap
1583        return None
1584
1585
1586class ColorTablesComboBox(PictureComboBox):
1587    """ComboBox with drawn color tables (created by thumbnails.py).
1588
1589    Used in r(3).colors dialog."""
1590
1591    def _getPath(self, name):
1592        return os.path.join(
1593            os.getenv("GISBASE"),
1594            "docs", "html", "colortables", "%s.png" % name)
1595
1596
1597class BarscalesComboBox(PictureComboBox):
1598    """ComboBox with barscales for d.barscale."""
1599
1600    def _getPath(self, name):
1601        return os.path.join(
1602            os.getenv("GISBASE"),
1603            "docs", "html", "barscales", name + '.png')
1604
1605
1606class NArrowsComboBox(PictureComboBox):
1607    """ComboBox with north arrows for d.barscale."""
1608
1609    def _getPath(self, name):
1610        return os.path.join(
1611            os.getenv("GISBASE"),
1612            "docs", "html", "northarrows", "%s.png" % name)
1613
1614
1615class LayersList(GListCtrl, listmix.TextEditMixin):
1616    """List of layers to be imported (dxf, shp...)"""
1617
1618    def __init__(self, parent, columns, log=None):
1619        GListCtrl.__init__(self, parent)
1620
1621        self.log = log
1622
1623        # setup mixins
1624        listmix.TextEditMixin.__init__(self)
1625
1626        for i in range(len(columns)):
1627            self.InsertColumn(i, columns[i])
1628
1629        width = []
1630        if len(columns) == 3:
1631            width = (65, 200)
1632        elif len(columns) == 4:
1633            width = (65, 200, 90)
1634        elif len(columns) == 5:
1635            width = (65, 180, 90, 70)
1636
1637        for i in range(len(width)):
1638            self.SetColumnWidth(col=i, width=width[i])
1639
1640    def OnLeftDown(self, event):
1641        """Allow editing only output name
1642
1643        Code taken from TextEditMixin class.
1644        """
1645        x, y = event.GetPosition()
1646
1647        colLocs = [0]
1648        loc = 0
1649        for n in range(self.GetColumnCount()):
1650            loc = loc + self.GetColumnWidth(n)
1651            colLocs.append(loc)
1652
1653        col = bisect(colLocs, x + self.GetScrollPos(wx.HORIZONTAL)) - 1
1654
1655        if col == self.GetColumnCount() - 1:
1656            listmix.TextEditMixin.OnLeftDown(self, event)
1657        else:
1658            event.Skip()
1659
1660    def GetLayers(self):
1661        """Get list of layers (layer name, output name, list id)"""
1662        layers = []
1663
1664        data = self.GetData(checked=True)
1665
1666        for itm in data:
1667
1668            layer = itm[1]
1669            ftype = itm[2]
1670            if '/' in ftype:
1671                layer += '|%s' % ftype.split('/', 1)[0]
1672            output = itm[self.GetColumnCount() - 1]
1673            layers.append((layer, output, itm[-1]))
1674
1675        return layers
1676