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