/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/UI/mainwindow.py
ViewVC logotype

Contents of /branches/WIP-pyshapelib-bramz/Thuban/UI/mainwindow.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2734 - (show annotations)
Thu Mar 1 12:42:59 2007 UTC (18 years ago) by bramz
File MIME type: text/x-python
File size: 53516 byte(s)
made a copy
1 # Copyright (C) 2001, 2002, 2003, 2004, 2005 by Intevation GmbH
2 # Authors:
3 # Jan-Oliver Wagner <[email protected]>
4 # Bernhard Herzog <[email protected]>
5 # Frank Koormann <[email protected]>
6 #
7 # This program is free software under the GPL (>=v2)
8 # Read the file COPYING coming with Thuban for details.
9
10 """
11 The main window
12 """
13
14 __version__ = "$Revision$"
15 # $Source$
16 # $Id$
17
18 import os
19 import copy
20
21 import wx
22
23 import Thuban
24
25 from Thuban import _
26 from Thuban.Model.messages import TITLE_CHANGED, LAYER_PROJECTION_CHANGED, \
27 MAP_PROJECTION_CHANGED, MAP_LAYERS_ADDED, MAP_LAYERS_REMOVED
28
29 from Thuban.Model.session import create_empty_session
30 from Thuban.Model.layer import Layer, RasterLayer
31 from Thuban.Model.postgisdb import PostGISShapeStore, has_postgis_support
32 from wx.lib.dialogs import MultipleChoiceDialog
33
34 import view
35 import tree
36 import tableview, identifyview
37 import legend
38 from menu import Menu
39
40 from context import Context
41 from command import registry, Command, ToolCommand
42 from messages import LAYER_SELECTED, SHAPES_SELECTED, VIEW_POSITION, \
43 MAP_REPLACED
44 from about import About
45
46 from Thuban.UI.dock import DockFrame
47 from Thuban.UI.join import JoinDialog
48 from Thuban.UI.dbdialog import DBFrame, DBDialog, ChooseDBTableDialog
49 import resource
50 import Thuban.Model.resource
51
52 import projdialog
53
54 from Thuban.UI.classifier import Classifier
55 from Thuban.UI.rasterlayerproperties import RasterLayerProperties
56 from Thuban.Model.layer import RasterLayer
57
58 from Thuban.Lib.classmapper import ClassMapper
59
60 layer_properties_dialogs = ClassMapper()
61 layer_properties_dialogs.add(RasterLayer, RasterLayerProperties)
62 layer_properties_dialogs.add(Layer, Classifier)
63
64 class MainWindow(DockFrame):
65
66 # Some messages that can be subscribed/unsubscribed directly through
67 # the MapCanvas come in fact from other objects. This is a map to
68 # map those messages to the names of the instance variables they
69 # actually come from. This delegation is implemented in the
70 # Subscribe and unsubscribed methods
71 delegated_messages = {LAYER_SELECTED: "canvas",
72 SHAPES_SELECTED: "canvas",
73 MAP_REPLACED: "canvas"}
74
75 # Methods delegated to some instance variables. The delegation is
76 # implemented in the __getattr__ method.
77 delegated_methods = {"SelectLayer": "canvas",
78 "SelectShapes": "canvas",
79 "SelectedLayer": "canvas",
80 "SelectedShapes": "canvas",
81 }
82
83 # Messages from the canvas that may require a status bar update.
84 # The update_status_bar method will be subscribed to these messages.
85 update_status_bar_messages = (VIEW_POSITION, LAYER_PROJECTION_CHANGED,
86 MAP_PROJECTION_CHANGED, MAP_LAYERS_ADDED,
87 MAP_LAYERS_REMOVED)
88
89 def __init__(self, parent, ID, title, application, interactor,
90 initial_message = None, size = wx.Size(-1, -1)):
91 DockFrame.__init__(self, parent, ID, title, wx.DefaultPosition, size)
92 #wxFrame.__init__(self, parent, ID, title, wxDefaultPosition, size)
93
94 self.application = application
95
96 self.CreateStatusBar()
97 if initial_message:
98 self.SetStatusText(initial_message)
99
100 self.identify_view = None
101
102 self.init_ids()
103
104 # creat the menubar from the main_menu description
105 self.SetMenuBar(self.build_menu_bar(main_menu))
106
107 # Similarly, create the toolbar from main_toolbar
108 toolbar = self.build_toolbar(main_toolbar)
109 # call Realize to make sure that the tools appear.
110 toolbar.Realize()
111
112
113 # Create the map canvas
114 canvas = view.MapCanvas(self, -1)
115 canvas.Subscribe(SHAPES_SELECTED, self.identify_view_on_demand)
116 self.canvas = canvas
117 self.canvas.Subscribe(TITLE_CHANGED, self.title_changed)
118
119 for channel in self.update_status_bar_messages:
120 self.canvas.Subscribe(channel, self.update_status_bar)
121
122 self.SetMainWindow(self.canvas)
123
124 self.SetAutoLayout(True)
125
126 self.init_dialogs()
127
128 self.ShowLegend()
129
130 self.Bind(wx.EVT_CLOSE, self.OnClose)
131
132 def Subscribe(self, channel, *args):
133 """Subscribe a function to a message channel.
134
135 If channel is one of the delegated messages call the appropriate
136 object's Subscribe method. Otherwise do nothing.
137 """
138 if channel in self.delegated_messages:
139 object = getattr(self, self.delegated_messages[channel])
140 object.Subscribe(channel, *args)
141 else:
142 print "Trying to subscribe to unsupported channel %s" % channel
143
144 def Unsubscribe(self, channel, *args):
145 """Unsubscribe a function from a message channel.
146
147 If channel is one of the delegated messages call the appropriate
148 object's Unsubscribe method. Otherwise do nothing.
149 """
150 if channel in self.delegated_messages:
151 object = getattr(self, self.delegated_messages[channel])
152 try:
153 object.Unsubscribe(channel, *args)
154 except wx.PyDeadObjectError:
155 # The object was a wxObject and has already been
156 # destroyed. Hopefully it has unsubscribed all its
157 # subscribers already so that it's OK if we do nothing
158 # here
159 pass
160
161 def __getattr__(self, attr):
162 """If attr is one of the delegated methods return that method
163
164 Otherwise raise AttributeError.
165 """
166 if attr in self.delegated_methods:
167 return getattr(getattr(self, self.delegated_methods[attr]), attr)
168 raise AttributeError(attr)
169
170 def init_ids(self):
171 """Initialize the ids"""
172 self.current_id = 6000
173 self.id_to_name = {}
174 self.name_to_id = {}
175 self.events_bound = {}
176
177 def get_id(self, name):
178 """Return the wxWindows id for the command named name.
179
180 Create a new one if there isn't one yet"""
181 ID = self.name_to_id.get(name)
182 if ID is None:
183 ID = self.current_id
184 self.current_id = self.current_id + 1
185 self.name_to_id[name] = ID
186 self.id_to_name[ID] = name
187 return ID
188
189 def bind_command_events(self, command, ID):
190 """Bind the necessary events for the given command and ID"""
191 if not self.events_bound.has_key(ID):
192 # the events haven't been bound yet
193 self.Bind(wx.EVT_MENU, self.invoke_command, id=ID)
194 if command.IsDynamic():
195 self.Bind(wx.EVT_UPDATE_UI, self.update_command_ui, id=ID)
196
197 def build_menu_bar(self, menudesc):
198 """Build and return the menu bar from the menu description"""
199 menu_bar = wx.MenuBar()
200
201 for item in menudesc.items:
202 # here the items must all be Menu instances themselves
203 menu_bar.Append(self.build_menu(item), item.title)
204
205 return menu_bar
206
207 def build_menu(self, menudesc):
208 """Return a wxMenu built from the menu description menudesc"""
209 wxmenu = wx.Menu()
210 last = None
211 for item in menudesc.items:
212 if item is None:
213 # a separator. Only add one if the last item was not a
214 # separator
215 if last is not None:
216 wxmenu.AppendSeparator()
217 elif isinstance(item, Menu):
218 # a submenu
219 wxmenu.AppendMenu(wx.NewId(), item.title, self.build_menu(item))
220 else:
221 # must the name the name of a command
222 self.add_menu_command(wxmenu, item)
223 last = item
224 return wxmenu
225
226 def build_toolbar(self, toolbardesc):
227 """Build and return the main toolbar window from a toolbar description
228
229 The parameter should be an instance of the Menu class but it
230 should not contain submenus.
231 """
232 toolbar = self.CreateToolBar(wx.TB_3DBUTTONS)
233
234 # set the size of the tools' bitmaps. Not needed on wxGTK, but
235 # on Windows, although it doesn't work very well there. It seems
236 # that only 16x16 icons are really supported on windows.
237 # We probably shouldn't hardwire the bitmap size here.
238 toolbar.SetToolBitmapSize(wx.Size(24, 24))
239
240 for item in toolbardesc.items:
241 if item is None:
242 toolbar.AddSeparator()
243 else:
244 # assume it's a string.
245 self.add_toolbar_command(toolbar, item)
246
247 return toolbar
248
249 def add_menu_command(self, menu, name):
250 """Add the command with name name to the menu menu.
251
252 If name is None, add a separator.
253 """
254 if name is None:
255 menu.AppendSeparator()
256 else:
257 command = registry.Command(name)
258 if command is not None:
259 ID = self.get_id(name)
260 menu.Append(ID, command.Title(), command.HelpText(),
261 command.IsCheckCommand())
262 self.bind_command_events(command, ID)
263 else:
264 print _("Unknown command %s") % name
265
266 def add_toolbar_command(self, toolbar, name):
267 """Add the command with name name to the toolbar toolbar.
268
269 If name is None, add a separator.
270 """
271 # Assume that all toolbar commands are also menu commmands so
272 # that we don't have to add the event handlers here
273 if name is None:
274 toolbar.AddSeparator()
275 else:
276 command = registry.Command(name)
277 if command is not None:
278 ID = self.get_id(name)
279 bitmap = resource.GetBitmapResource(command.Icon(),
280 wx.BITMAP_TYPE_XPM)
281 toolbar.AddTool(ID, bitmap,
282 shortHelpString = command.HelpText(),
283 isToggle = command.IsCheckCommand())
284 self.bind_command_events(command, ID)
285 else:
286 print _("Unknown command %s") % name
287
288 def Context(self):
289 """Return the context object for a command invoked from this window
290 """
291 return Context(self.application, self.application.Session(), self)
292
293 def invoke_command(self, event):
294 name = self.id_to_name.get(event.GetId())
295 if name is not None:
296 command = registry.Command(name)
297 command.Execute(self.Context())
298 else:
299 print _("Unknown command ID %d") % event.GetId()
300
301 def update_command_ui(self, event):
302 #print "update_command_ui", self.id_to_name[event.GetId()]
303 context = self.Context()
304 command = registry.Command(self.id_to_name[event.GetId()])
305 if command is not None:
306 sensitive = command.Sensitive(context)
307 event.Enable(sensitive)
308 if command.IsTool() and not sensitive and command.Checked(context):
309 # When a checked tool command is disabled deselect all
310 # tools. Otherwise the tool would remain active but it
311 # might lead to errors if the tools stays active. This
312 # problem occurred in GREAT-ER and this fixes it, but
313 # it's not clear to me whether this is really the best
314 # way to do it (BH, 20021206).
315 self.canvas.SelectTool(None)
316 event.SetText(command.DynText(context))
317 if command.IsCheckCommand():
318 event.Check(command.Checked(context))
319
320 def RunMessageBox(self, title, text, flags = wx.OK | wx.ICON_INFORMATION):
321 """Run a modal message box with the given text, title and flags
322 and return the result"""
323 dlg = wx.MessageDialog(self, text, title, flags)
324 dlg.CenterOnParent()
325 result = dlg.ShowModal()
326 dlg.Destroy()
327 return result
328
329 def init_dialogs(self):
330 """Initialize the dialog handling"""
331 # The mainwindow maintains a dict mapping names to open
332 # non-modal dialogs. The dialogs are put into this dict when
333 # they're created and removed when they're closed
334 self.dialogs = {}
335
336 def add_dialog(self, name, dialog):
337 if self.dialogs.has_key(name):
338 raise RuntimeError(_("The Dialog named %s is already open") % name)
339 self.dialogs[name] = dialog
340
341 def dialog_open(self, name):
342 return self.dialogs.has_key(name)
343
344 def remove_dialog(self, name):
345 del self.dialogs[name]
346
347 def get_open_dialog(self, name):
348 return self.dialogs.get(name)
349
350 def update_status_bar(self, *args):
351 """Handler for a bunch of messages that may require a status bar update
352
353 Currently this handles the canvas' VIEW_POSITION_CHANGED
354 messages as well as several messages about changes in the map
355 which may affect whether and how projections are used.
356
357 These messages affect the text in the status bar in the following way:
358
359 When VIEW_POSITION_CHANGED messages are sent and the mouse is
360 actually in the canvas window, display the current mouse
361 coordinates as defined by the canvas' CurrentPosition method.
362
363 If there is no current position to show, check whether there is
364 a potential problem with the map and layer projections and
365 display a message about it. Otherwise the status bar will
366 become empty.
367
368 The text is displayed in the status bar using the
369 set_position_text method.
370 """
371 # Implementation note: We do not really have to know which
372 # message was sent. We can simply call the canvas'
373 # CurrentPosition method and if that returns a tuple, it was a
374 # VIEW_POSITION_CHANGED message and we have to display it.
375 # Otherwise it was a VIEW_POSITION_CHANGED message where the
376 # mouse has left the canvas or it was a message about a change
377 # to the map, in which case we check the projections.
378 #
379 # When changing this method, keep in mind that the
380 # VIEW_POSITION_CHANGED message are sent for every mouse move in
381 # the canvas window, that is they happen very often, so the path
382 # taken in that case has to be fast.
383 text = ""
384 pos = self.canvas.CurrentPosition()
385 if pos is not None:
386 text = "(%10.10g, %10.10g)" % pos
387 else:
388 for layer in self.canvas.Map().Layers():
389 bbox = layer.LatLongBoundingBox()
390 if bbox:
391 left, bottom, right, top = bbox
392 if not (-180 <= left <= 180 and
393 -180 <= right <= 180 and
394 -90 <= top <= 90 and
395 -90 <= bottom <= 90):
396 text = _("Select layer '%s' and pick a projection "
397 "using Layer/Projection...") % layer.title
398 break
399
400 self.set_position_text(text)
401
402 def set_position_text(self, text):
403 """Set the statusbar text to that created by update_status_bar
404
405 By default the text is shown in field 0 of the status bar.
406 Override this method in derived classes to put it into a
407 different field of the statusbar.
408
409 For historical reasons this method is called set_position_text
410 because at first the text was always either the current position
411 or the empty string. Now it can contain other messages as well.
412 The method will be renamed at one point.
413 """
414 # Note: If this method is renamed we should perhaps think about
415 # some backwards compatibility measures for projects like
416 # GREAT-ER which override this method.
417 self.SetStatusText(text)
418
419 def OpenOrRaiseDialog(self, name, dialog_class, *args, **kw):
420 """
421 Open or raise a dialog.
422
423 If a dialog with the denoted name does already exist it is
424 raised. Otherwise a new dialog, an instance of dialog_class,
425 is created, inserted into the main list and displayed.
426 """
427 dialog = self.get_open_dialog(name)
428
429 if dialog is None:
430 dialog = dialog_class(self, name, *args, **kw)
431 self.add_dialog(name, dialog)
432 dialog.Show(True)
433 else:
434 dialog.Raise()
435
436 def save_modified_session(self, can_veto = 1):
437 """If the current session has been modified, ask the user
438 whether to save it and do so if requested. Return the outcome of
439 the dialog (either wxID_OK, wxID_CANCEL or wxID_NO). If the
440 dialog wasn't run return wxID_NO.
441
442 If the can_veto parameter is true (default) the dialog includes
443 a cancel button, otherwise not.
444 """
445 if self.application.session.WasModified():
446 flags = wx.YES_NO | wx.ICON_QUESTION
447 if can_veto:
448 flags = flags | wx.CANCEL
449 result = self.RunMessageBox(_("Exit"),
450 _("The session has been modified."
451 " Do you want to save it?"),
452 flags)
453 if result == wx.ID_YES:
454 self.SaveSession()
455 else:
456 result = wx.ID_NO
457 return result
458
459 def NewSession(self):
460 if self.save_modified_session() != wx.ID_CANCEL:
461 self.application.SetSession(create_empty_session())
462
463 def OpenSession(self):
464 if self.save_modified_session() != wx.ID_CANCEL:
465 dlg = wx.FileDialog(self, _("Open Session"),
466 self.application.Path("data"), "",
467 "Thuban Session File (*.thuban)|*.thuban",
468 wx.OPEN)
469 if dlg.ShowModal() == wx.ID_OK:
470 self.application.OpenSession(dlg.GetPath(),
471 self.run_db_param_dialog)
472 self.application.SetPath("data", dlg.GetPath())
473 dlg.Destroy()
474
475 def run_db_param_dialog(self, parameters, message):
476 dlg = DBDialog(self, _("DB Connection Parameters"), parameters,
477 message)
478 return dlg.RunDialog()
479
480 def SaveSession(self):
481 if self.application.session.filename == None:
482 self.SaveSessionAs()
483 else:
484 self.application.SaveSession()
485
486 def SaveSessionAs(self):
487 dlg = wx.FileDialog(self, _("Save Session As"),
488 self.application.Path("data"), "",
489 "Thuban Session File (*.thuban)|*.thuban",
490 wx.SAVE|wx.OVERWRITE_PROMPT)
491 if dlg.ShowModal() == wx.ID_OK:
492 self.application.session.SetFilename(dlg.GetPath())
493 self.application.SaveSession()
494 self.application.SetPath("data",dlg.GetPath())
495 dlg.Destroy()
496
497 def Exit(self):
498 self.Close(False)
499
500 def OnClose(self, event):
501 result = self.save_modified_session(can_veto = event.CanVeto())
502 if result == wx.ID_CANCEL:
503 event.Veto()
504 else:
505 # FIXME: it would be better to tie the unsubscription to
506 # wx's destroy event, but that isn't implemented for wxGTK
507 # yet.
508 for channel in self.update_status_bar_messages:
509 self.canvas.Unsubscribe(channel, self.update_status_bar)
510
511 DockFrame.OnClose(self, event)
512 for dlg in self.dialogs.values():
513 dlg.Destroy()
514 self.canvas.Destroy()
515 self.Destroy()
516
517 def SetMap(self, map):
518 self.canvas.SetMap(map)
519 self.update_title()
520
521 dialog = self.FindRegisteredDock("legend")
522 if dialog is not None:
523 dialog.GetPanel().SetMap(self.Map())
524
525 def Map(self):
526 """Return the map displayed by this mainwindow"""
527
528 return self.canvas.Map()
529
530 def ToggleSessionTree(self):
531 """If the session tree is shown close it otherwise create a new tree"""
532 name = "session_tree"
533 dialog = self.get_open_dialog(name)
534 if dialog is None:
535 dialog = tree.SessionTreeView(self, self.application, name)
536 self.add_dialog(name, dialog)
537 dialog.Show(True)
538 else:
539 dialog.Close()
540
541 def SessionTreeShown(self):
542 """Return true iff the session tree is currently shown"""
543 return self.get_open_dialog("session_tree") is not None
544
545 def About(self):
546 dlg = About(self)
547 dlg.ShowModal()
548 dlg.Destroy()
549
550 def DatabaseManagement(self):
551 name = "dbmanagement"
552 dialog = self.get_open_dialog(name)
553 if dialog is None:
554 map = self.canvas.Map()
555 dialog = DBFrame(self, name, self.application.Session())
556 self.add_dialog(name, dialog)
557 dialog.Show()
558 dialog.Raise()
559
560 def AddLayer(self):
561 dlg = wx.FileDialog(self, _("Select one or more data files"),
562 self.application.Path("data"), "",
563 _("Shapefiles (*.shp)") + "|*.shp;*.SHP|" +
564 _("All Files (*.*)") + "|*.*",
565 wx.OPEN | wx.MULTIPLE)
566 if dlg.ShowModal() == wx.ID_OK:
567 filenames = dlg.GetPaths()
568 for filename in filenames:
569 title = os.path.splitext(os.path.basename(filename))[0]
570 map = self.canvas.Map()
571 has_layers = map.HasLayers()
572 try:
573 store = self.application.Session().OpenShapefile(filename)
574 except IOError:
575 # the layer couldn't be opened
576 self.RunMessageBox(_("Add Layer"),
577 _("Can't open the file '%s'.")%filename)
578 else:
579 layer = Layer(title, store)
580 map.AddLayer(layer)
581 if not has_layers:
582 # if we're adding a layer to an empty map, fit the
583 # new map to the window
584 self.canvas.FitMapToWindow()
585 self.application.SetPath("data",filename)
586 dlg.Destroy()
587
588 def AddRasterLayer(self):
589 dlg = wx.FileDialog(self, _("Select an image file"),
590 self.application.Path("data"), "", "*.*",
591 wx.OPEN)
592 if dlg.ShowModal() == wx.ID_OK:
593 filename = dlg.GetPath()
594 title = os.path.splitext(os.path.basename(filename))[0]
595 map = self.canvas.Map()
596 has_layers = map.HasLayers()
597 try:
598 layer = RasterLayer(title, filename)
599 except IOError:
600 # the layer couldn't be opened
601 self.RunMessageBox(_("Add Image Layer"),
602 _("Can't open the file '%s'.") % filename)
603 else:
604 map.AddLayer(layer)
605 if not has_layers:
606 # if we're adding a layer to an empty map, fit the
607 # new map to the window
608 self.canvas.FitMapToWindow()
609 self.application.SetPath("data", filename)
610 dlg.Destroy()
611
612 def AddDBLayer(self):
613 """Add a layer read from a database"""
614 session = self.application.Session()
615 dlg = ChooseDBTableDialog(self, self.application.Session())
616
617 if dlg.ShowModal() == wx.ID_OK:
618 dbconn, dbtable, id_column, geo_column = dlg.GetTable()
619 try:
620 title = str(dbtable)
621
622 # Chose the correct Interface for the database type
623 store = session.OpenDBShapeStore(dbconn, dbtable,
624 id_column = id_column,
625 geometry_column = geo_column)
626 layer = Layer(title, store)
627 except:
628 # Some error occured while initializing the layer
629 self.RunMessageBox(_("Add Layer from database"),
630 _("Can't open the database table '%s'")
631 % dbtable)
632 return
633
634 map = self.canvas.Map()
635
636 has_layers = map.HasLayers()
637 map.AddLayer(layer)
638 if not has_layers:
639 self.canvas.FitMapToWindow()
640
641 dlg.Destroy()
642
643 def RemoveLayer(self):
644 layer = self.current_layer()
645 if layer is not None:
646 self.canvas.Map().RemoveLayer(layer)
647
648 def CanRemoveLayer(self):
649 """Return true if the currently selected layer can be deleted.
650
651 If no layer is selected return False.
652
653 The return value of this method determines whether the remove
654 layer command is sensitive in menu.
655 """
656 layer = self.current_layer()
657 if layer is not None:
658 return self.canvas.Map().CanRemoveLayer(layer)
659 return False
660
661 def LayerToTop(self):
662 layer = self.current_layer()
663 if layer is not None:
664 self.canvas.Map().MoveLayerToTop(layer)
665
666 def RaiseLayer(self):
667 layer = self.current_layer()
668 if layer is not None:
669 self.canvas.Map().RaiseLayer(layer)
670
671 def LowerLayer(self):
672 layer = self.current_layer()
673 if layer is not None:
674 self.canvas.Map().LowerLayer(layer)
675
676 def LayerToBottom(self):
677 layer = self.current_layer()
678 if layer is not None:
679 self.canvas.Map().MoveLayerToBottom(layer)
680
681 def current_layer(self):
682 """Return the currently selected layer.
683
684 If no layer is selected, return None
685 """
686 return self.canvas.SelectedLayer()
687
688 def has_selected_layer(self):
689 """Return true if a layer is currently selected"""
690 return self.canvas.HasSelectedLayer()
691
692 def has_selected_shape_layer(self):
693 """Return true if a shape layer is currently selected"""
694 return isinstance(self.current_layer(), Layer)
695
696 def has_selected_shapes(self):
697 """Return true if a shape is currently selected"""
698 return self.canvas.HasSelectedShapes()
699
700 def HideLayer(self):
701 layer = self.current_layer()
702 if layer is not None:
703 layer.SetVisible(False)
704
705 def ShowLayer(self):
706 layer = self.current_layer()
707 if layer is not None:
708 layer.SetVisible(True)
709
710 def ToggleLayerVisibility(self):
711 layer = self.current_layer()
712 layer.SetVisible(not layer.Visible())
713
714 def DuplicateLayer(self):
715 """Ceate a new layer above the selected layer with the same shapestore
716 """
717 layer = self.current_layer()
718 if layer is not None and hasattr(layer, "ShapeStore"):
719 new_layer = Layer(_("Copy of `%s'") % layer.Title(),
720 layer.ShapeStore(),
721 projection = layer.GetProjection())
722 new_classification = copy.deepcopy(layer.GetClassification())
723 new_layer.SetClassificationColumn(
724 layer.GetClassificationColumn())
725 new_layer.SetClassification(new_classification)
726 self.Map().AddLayer(new_layer)
727
728 def CanDuplicateLayer(self):
729 """Return whether the DuplicateLayer method can create a duplicate"""
730 layer = self.current_layer()
731 return layer is not None and hasattr(layer, "ShapeStore")
732
733 def LayerShowTable(self):
734 """
735 Present a TableView Window for the current layer.
736 In case the window is already open, bring it to the front.
737 In case, there is no active layer, do nothing.
738 In case, the layer has no ShapeStore, do nothing.
739 """
740 layer = self.current_layer()
741 if layer is not None:
742 if not hasattr(layer, "ShapeStore"):
743 return
744 table = layer.ShapeStore().Table()
745 name = "table_view" + str(id(table))
746 dialog = self.get_open_dialog(name)
747 if dialog is None:
748 dialog = tableview.LayerTableFrame(self, name,
749 _("Layer Table: %s") % layer.Title(),
750 layer, table)
751 self.add_dialog(name, dialog)
752 dialog.Show(True)
753 else:
754 dialog.Raise()
755
756 def MapProjection(self):
757
758 name = "map_projection"
759 dialog = self.get_open_dialog(name)
760
761 if dialog is None:
762 map = self.canvas.Map()
763 dialog = projdialog.ProjFrame(self, name,
764 _("Map Projection: %s") % map.Title(), map)
765 self.add_dialog(name, dialog)
766 dialog.Show()
767 dialog.Raise()
768
769 def LayerProjection(self):
770
771 layer = self.current_layer()
772
773 name = "layer_projection" + str(id(layer))
774 dialog = self.get_open_dialog(name)
775
776 if dialog is None:
777 map = self.canvas.Map()
778 dialog = projdialog.ProjFrame(self, name,
779 _("Layer Projection: %s") % layer.Title(), layer)
780 self.add_dialog(name, dialog)
781 dialog.Show()
782 dialog.Raise()
783
784 def LayerEditProperties(self):
785
786 #
787 # the menu option for this should only be available if there
788 # is a current layer, so we don't need to check if the
789 # current layer is None
790 #
791
792 layer = self.current_layer()
793 self.OpenLayerProperties(layer)
794
795 def OpenLayerProperties(self, layer, group = None):
796 """
797 Open or raise the properties dialog.
798
799 This method opens or raises the properties dialog for the
800 currently selected layer if one is defined for this layer
801 type.
802 """
803 dialog_class = layer_properties_dialogs.get(layer)
804
805 if dialog_class is not None:
806 name = "layer_properties" + str(id(layer))
807 self.OpenOrRaiseDialog(name, dialog_class, layer, group = group)
808
809 def LayerJoinTable(self):
810 layer = self.canvas.SelectedLayer()
811 if layer is not None:
812 dlg = JoinDialog(self, _("Join Layer with Table"),
813 self.application.session,
814 layer = layer)
815 dlg.ShowModal()
816
817 def LayerUnjoinTable(self):
818 layer = self.canvas.SelectedLayer()
819 if layer is not None:
820 orig_store = layer.ShapeStore().OrigShapeStore()
821 if orig_store:
822 layer.SetShapeStore(orig_store)
823
824 def ShowLegend(self):
825 if not self.LegendShown():
826 self.ToggleLegend()
827
828 def ToggleLegend(self):
829 """Show the legend if it's not shown otherwise hide it again"""
830 name = "legend"
831 dialog = self.FindRegisteredDock(name)
832
833 if dialog is None:
834 dialog = self.CreateDock(name, -1, _("Legend"), wx.LAYOUT_LEFT)
835 legend.LegendPanel(dialog, None, self)
836 dialog.Dock()
837 dialog.GetPanel().SetMap(self.Map())
838 dialog.Show()
839 else:
840 dialog.Show(not dialog.IsShown())
841
842 def LegendShown(self):
843 """Return true iff the legend is currently open"""
844 dialog = self.FindRegisteredDock("legend")
845 return dialog is not None and dialog.IsShown()
846
847 def TableOpen(self):
848 dlg = wx.FileDialog(self, _("Open Table"),
849 self.application.Path("data"), "",
850 _("DBF Files (*.dbf)") + "|*.dbf|" +
851 #_("CSV Files (*.csv)") + "|*.csv|" +
852 _("All Files (*.*)") + "|*.*",
853 wx.OPEN)
854 if dlg.ShowModal() == wx.ID_OK:
855 filename = dlg.GetPath()
856 dlg.Destroy()
857 try:
858 table = self.application.session.OpenTableFile(filename)
859 except IOError:
860 # the layer couldn't be opened
861 self.RunMessageBox(_("Open Table"),
862 _("Can't open the file '%s'.") % filename)
863 else:
864 self.ShowTableView(table)
865 self.application.SetPath("data",filename)
866
867 def TableClose(self):
868 tables = self.application.session.UnreferencedTables()
869
870 lst = [(t.Title(), t) for t in tables]
871 lst.sort()
872 titles = [i[0] for i in lst]
873 dlg = MultipleChoiceDialog(self, _("Pick the tables to close:"),
874 _("Close Table"), titles,
875 size = (400, 300),
876 style = wx.DEFAULT_DIALOG_STYLE |
877 wx.RESIZE_BORDER)
878 if dlg.ShowModal() == wx.ID_OK:
879 for i in dlg.GetValue():
880 self.application.session.RemoveTable(lst[i][1])
881
882
883 def TableShow(self):
884 """Offer a multi-selection dialog for tables to be displayed
885
886 The windows for the selected tables are opened or brought to
887 the front.
888 """
889 tables = self.application.session.Tables()
890
891 lst = [(t.Title(), t) for t in tables]
892 lst.sort()
893 titles = [i[0] for i in lst]
894 dlg = MultipleChoiceDialog(self, _("Pick the table to show:"),
895 _("Show Table"), titles,
896 size = (400,300),
897 style = wx.DEFAULT_DIALOG_STYLE |
898 wx.RESIZE_BORDER)
899 if (dlg.ShowModal() == wx.ID_OK):
900 for i in dlg.GetValue():
901 # XXX: if the table belongs to a layer, open a
902 # LayerTableFrame instead of QueryTableFrame
903 self.ShowTableView(lst[i][1])
904
905 def TableJoin(self):
906 dlg = JoinDialog(self, _("Join Tables"), self.application.session)
907 dlg.ShowModal()
908
909 def ShowTableView(self, table):
910 """Open a table view for the table and optionally"""
911 name = "table_view%d" % id(table)
912 dialog = self.get_open_dialog(name)
913 if dialog is None:
914 dialog = tableview.QueryTableFrame(self, name,
915 _("Table: %s") % table.Title(),
916 table)
917 self.add_dialog(name, dialog)
918 dialog.Show(True)
919 dialog.Raise()
920
921 def TableRename(self):
922 """Let the user rename a table"""
923
924 # First, let the user select a table
925 tables = self.application.session.Tables()
926 lst = [(t.Title(), t) for t in tables]
927 lst.sort()
928 titles = [i[0] for i in lst]
929 dlg = MultipleChoiceDialog(self, _("Pick the table to rename:"),
930 _("Rename Table"), titles,
931 size = (400,300),
932 style = wx.DEFAULT_DIALOG_STYLE |
933 wx.RESIZE_BORDER)
934 if (dlg.ShowModal() == wx.ID_OK):
935 to_rename = [lst[i][1] for i in dlg.GetValue()]
936 dlg.Destroy()
937 else:
938 to_rename = []
939
940 # Second, let the user rename the layers
941 for table in to_rename:
942 dlg = wx.TextEntryDialog(self, _("Table Title:"), _("Rename Table"),
943 table.Title())
944 try:
945 if dlg.ShowModal() == wx.ID_OK:
946 title = dlg.GetValue()
947 if title != "":
948 table.SetTitle(title)
949
950 # Make sure the session is marked as modified.
951 # FIXME: This should be handled automatically,
952 # but that requires more changes to the tables
953 # than I have time for currently.
954 self.application.session.changed()
955 finally:
956 dlg.Destroy()
957
958
959 def ZoomInTool(self):
960 self.canvas.ZoomInTool()
961
962 def ZoomOutTool(self):
963 self.canvas.ZoomOutTool()
964
965 def PanTool(self):
966 self.canvas.PanTool()
967
968 def IdentifyTool(self):
969 self.canvas.IdentifyTool()
970 self.identify_view_on_demand(None, None)
971
972 def LabelTool(self):
973 self.canvas.LabelTool()
974
975 def FullExtent(self):
976 self.canvas.FitMapToWindow()
977
978 def FullLayerExtent(self):
979 self.canvas.FitLayerToWindow(self.current_layer())
980
981 def FullSelectionExtent(self):
982 self.canvas.FitSelectedToWindow()
983
984 def ExportMap(self):
985 self.canvas.Export()
986
987 def PrintMap(self):
988 self.canvas.Print()
989
990 def RenameMap(self):
991 dlg = wx.TextEntryDialog(self, _("Map Title:"), _("Rename Map"),
992 self.Map().Title())
993 if dlg.ShowModal() == wx.ID_OK:
994 title = dlg.GetValue()
995 if title != "":
996 self.Map().SetTitle(title)
997
998 dlg.Destroy()
999
1000 def RenameLayer(self):
1001 """Let the user rename the currently selected layer"""
1002 layer = self.current_layer()
1003 if layer is not None:
1004 dlg = wx.TextEntryDialog(self, _("Layer Title:"), _("Rename Layer"),
1005 layer.Title())
1006 try:
1007 if dlg.ShowModal() == wx.ID_OK:
1008 title = dlg.GetValue()
1009 if title != "":
1010 layer.SetTitle(title)
1011 finally:
1012 dlg.Destroy()
1013
1014 def identify_view_on_demand(self, layer, shapes):
1015 """Subscribed to the canvas' SHAPES_SELECTED message
1016
1017 If the current tool is the identify tool, at least one shape is
1018 selected and the identify dialog is not shown, show the dialog.
1019 """
1020 # If the selection has become empty we don't need to do
1021 # anything. Otherwise it could happen that the dialog was popped
1022 # up when the selection became empty, e.g. when a new selection
1023 # is opened while the identify tool is active and dialog had
1024 # been closed
1025 if not shapes:
1026 return
1027
1028 name = "identify_view"
1029 if self.canvas.CurrentTool() == "IdentifyTool":
1030 if not self.dialog_open(name):
1031 dialog = identifyview.IdentifyView(self, name)
1032 self.add_dialog(name, dialog)
1033 dialog.Show(True)
1034 else:
1035 # FIXME: bring dialog to front?
1036 pass
1037
1038 def title_changed(self, map):
1039 """Subscribed to the canvas' TITLE_CHANGED messages"""
1040 self.update_title()
1041
1042 def update_title(self):
1043 """Update the window's title according to it's current state.
1044
1045 In this default implementation the title is 'Thuban - ' followed
1046 by the map's title or simply 'Thuban' if there is not map.
1047 Derived classes should override this method to get different
1048 titles.
1049
1050 This method is called automatically by other methods when the
1051 title may have to change. For the methods implemented in this
1052 class this usually only means that a different map has been set
1053 or the current map's title has changed.
1054 """
1055 map = self.Map()
1056 if map is not None:
1057 title = _("Thuban - %s") % (map.Title(),)
1058 else:
1059 title = _("Thuban")
1060 self.SetTitle(title)
1061
1062
1063 #
1064 # Define all the commands available in the main window
1065 #
1066
1067
1068 # Helper functions to define common command implementations
1069 def call_method(context, methodname, *args):
1070 """Call the mainwindow's method methodname with args *args"""
1071 apply(getattr(context.mainwindow, methodname), args)
1072
1073 def _method_command(name, title, method, helptext = "",
1074 icon = "", sensitive = None, checked = None):
1075 """Add a command implemented by a method of the mainwindow object"""
1076 registry.Add(Command(name, title, call_method, args=(method,),
1077 helptext = helptext, icon = icon,
1078 sensitive = sensitive, checked = checked))
1079
1080 def make_check_current_tool(toolname):
1081 """Return a function that tests if the currently active tool is toolname
1082
1083 The returned function can be called with the context and returns
1084 true iff the currently active tool's name is toolname. It's directly
1085 usable as the 'checked' callback of a command.
1086 """
1087 def check_current_tool(context, name=toolname):
1088 return context.mainwindow.canvas.CurrentTool() == name
1089 return check_current_tool
1090
1091 def _tool_command(name, title, method, toolname, helptext = "",
1092 icon = "", sensitive = None):
1093 """Add a tool command"""
1094 registry.Add(ToolCommand(name, title, call_method, args=(method,),
1095 helptext = helptext, icon = icon,
1096 checked = make_check_current_tool(toolname),
1097 sensitive = sensitive))
1098
1099 def _has_selected_layer(context):
1100 """Return true if a layer is selected in the context"""
1101 return context.mainwindow.has_selected_layer()
1102
1103 def _has_selected_layer_visible(context):
1104 """Return true if a layer is selected in the context which is
1105 visible."""
1106 if context.mainwindow.has_selected_layer():
1107 layer = context.mainwindow.current_layer()
1108 if layer.Visible(): return True
1109 return False
1110
1111 def _has_selected_shape_layer(context):
1112 """Return true if a shape layer is selected in the context"""
1113 return context.mainwindow.has_selected_shape_layer()
1114
1115 def _has_selected_shapes(context):
1116 """Return true if a layer is selected in the context"""
1117 return context.mainwindow.has_selected_shapes()
1118
1119 def _can_remove_layer(context):
1120 return context.mainwindow.CanRemoveLayer()
1121
1122 def _has_tree_window_shown(context):
1123 """Return true if the tree window is shown"""
1124 return context.mainwindow.SessionTreeShown()
1125
1126 def _has_visible_map(context):
1127 """Return true iff theres a visible map in the mainwindow.
1128
1129 A visible map is a map with at least one visible layer."""
1130 map = context.mainwindow.Map()
1131 if map is not None:
1132 for layer in map.Layers():
1133 if layer.Visible():
1134 return True
1135 return False
1136
1137 def _has_legend_shown(context):
1138 """Return true if the legend window is shown"""
1139 return context.mainwindow.LegendShown()
1140
1141 def _has_gdal_support(context):
1142 """Return True if the GDAL is available"""
1143 return Thuban.Model.resource.has_gdal_support()
1144
1145 def _has_dbconnections(context):
1146 """Return whether the the session has database connections"""
1147 return context.session.HasDBConnections()
1148
1149 def _has_postgis_support(context):
1150 return has_postgis_support()
1151
1152
1153 # File menu
1154 _method_command("new_session", _("&New Session"), "NewSession",
1155 helptext = _("Start a new session"))
1156 _method_command("open_session", _("&Open Session..."), "OpenSession",
1157 helptext = _("Open a session file"))
1158 _method_command("save_session", _("&Save Session"), "SaveSession",
1159 helptext =_("Save this session to the file it was opened from"))
1160 _method_command("save_session_as", _("Save Session &As..."), "SaveSessionAs",
1161 helptext = _("Save this session to a new file"))
1162 _method_command("toggle_session_tree", _("Session &Tree"), "ToggleSessionTree",
1163 checked = _has_tree_window_shown,
1164 helptext = _("Toggle on/off the session tree analysis window"))
1165 _method_command("toggle_legend", _("Legend"), "ToggleLegend",
1166 checked = _has_legend_shown,
1167 helptext = _("Toggle Legend on/off"))
1168 _method_command("database_management", _("&Database Connections..."),
1169 "DatabaseManagement",
1170 sensitive = _has_postgis_support)
1171 _method_command("exit", _("E&xit"), "Exit",
1172 helptext = _("Finish working with Thuban"))
1173
1174 # Help menu
1175 _method_command("help_about", _("&About..."), "About",
1176 helptext = _("Info about Thuban authors, version and modules"))
1177
1178
1179 # Map menu
1180 _method_command("map_projection", _("Pro&jection..."), "MapProjection",
1181 helptext = _("Set or change the map projection"))
1182
1183 _tool_command("map_zoom_in_tool", _("&Zoom in"), "ZoomInTool", "ZoomInTool",
1184 helptext = _("Switch to map-mode 'zoom-in'"), icon = "zoom_in",
1185 sensitive = _has_visible_map)
1186 _tool_command("map_zoom_out_tool", _("Zoom &out"), "ZoomOutTool", "ZoomOutTool",
1187 helptext = _("Switch to map-mode 'zoom-out'"), icon = "zoom_out",
1188 sensitive = _has_visible_map)
1189 _tool_command("map_pan_tool", _("&Pan"), "PanTool", "PanTool",
1190 helptext = _("Switch to map-mode 'pan'"), icon = "pan",
1191 sensitive = _has_visible_map)
1192 _tool_command("map_identify_tool", _("&Identify"), "IdentifyTool",
1193 "IdentifyTool",
1194 helptext = _("Switch to map-mode 'identify'"), icon = "identify",
1195 sensitive = _has_visible_map)
1196 _tool_command("map_label_tool", _("&Label"), "LabelTool", "LabelTool",
1197 helptext = _("Add/Remove labels"), icon = "label",
1198 sensitive = _has_visible_map)
1199 _method_command("map_full_extent", _("&Full extent"), "FullExtent",
1200 helptext = _("Zoom to the full map extent"), icon = "fullextent",
1201 sensitive = _has_visible_map)
1202 _method_command("layer_full_extent", _("&Full layer extent"), "FullLayerExtent",
1203 helptext = _("Zoom to the full layer extent"),
1204 icon = "fulllayerextent", sensitive = _has_selected_layer)
1205 _method_command("selected_full_extent", _("&Full selection extent"),
1206 "FullSelectionExtent",
1207 helptext = _("Zoom to the full selection extent"),
1208 icon = "fullselextent", sensitive = _has_selected_shapes)
1209 _method_command("map_export", _("E&xport"), "ExportMap",
1210 helptext = _("Export the map to file"))
1211 _method_command("map_print", _("Prin&t"), "PrintMap",
1212 helptext = _("Print the map"))
1213 _method_command("map_rename", _("&Rename..."), "RenameMap",
1214 helptext = _("Rename the map"))
1215 _method_command("layer_add", _("&Add Layer..."), "AddLayer",
1216 helptext = _("Add a new layer to the map"))
1217 _method_command("rasterlayer_add", _("&Add Image Layer..."), "AddRasterLayer",
1218 helptext = _("Add a new image layer to the map"),
1219 sensitive = _has_gdal_support)
1220 _method_command("layer_add_db", _("Add &Database Layer..."), "AddDBLayer",
1221 helptext = _("Add a new database layer to active map"),
1222 sensitive = _has_dbconnections)
1223 _method_command("layer_remove", _("&Remove Layer"), "RemoveLayer",
1224 helptext = _("Remove selected layer"),
1225 sensitive = _can_remove_layer)
1226
1227 # Layer menu
1228 _method_command("layer_projection", _("Pro&jection..."), "LayerProjection",
1229 sensitive = _has_selected_layer,
1230 helptext = _("Specify projection for selected layer"))
1231 _method_command("layer_duplicate", _("&Duplicate"), "DuplicateLayer",
1232 helptext = _("Duplicate selected layer"),
1233 sensitive = lambda context: context.mainwindow.CanDuplicateLayer())
1234 _method_command("layer_rename", _("Re&name ..."), "RenameLayer",
1235 helptext = _("Rename selected layer"),
1236 sensitive = _has_selected_layer)
1237 _method_command("layer_raise", _("&Raise"), "RaiseLayer",
1238 helptext = _("Raise selected layer"),
1239 sensitive = _has_selected_layer)
1240 _method_command("layer_lower", _("&Lower"), "LowerLayer",
1241 helptext = _("Lower selected layer"),
1242 sensitive = _has_selected_layer)
1243 _method_command("layer_show", _("&Show"), "ShowLayer",
1244 helptext = _("Make selected layer visible"),
1245 sensitive = _has_selected_layer)
1246 _method_command("layer_hide", _("&Hide"), "HideLayer",
1247 helptext = _("Make selected layer unvisible"),
1248 sensitive = _has_selected_layer)
1249 _method_command("layer_show_table", _("Show Ta&ble"), "LayerShowTable",
1250 helptext = _("Show the selected layer's table"),
1251 sensitive = _has_selected_shape_layer)
1252 _method_command("layer_properties", _("&Properties..."), "LayerEditProperties",
1253 sensitive = _has_selected_layer,
1254 helptext = _("Edit the properties of the selected layer"))
1255 _method_command("layer_jointable", _("&Join Table..."), "LayerJoinTable",
1256 sensitive = _has_selected_shape_layer,
1257 helptext = _("Join and attach a table to the selected layer"))
1258
1259 # further layer methods:
1260 _method_command("layer_to_top", _("&Top"), "LayerToTop",
1261 helptext = _("Put selected layer to the top"),
1262 sensitive = _has_selected_layer)
1263 _method_command("layer_to_bottom", _("&Bottom"), "LayerToBottom",
1264 helptext = _("Put selected layer to the bottom"),
1265 sensitive = _has_selected_layer)
1266 _method_command("layer_visibility", _("&Visible"), "ToggleLayerVisibility",
1267 checked = _has_selected_layer_visible,
1268 helptext = _("Toggle visibility of selected layer"),
1269 sensitive = _has_selected_layer)
1270
1271 def _can_unjoin(context):
1272 """Return whether the Layer/Unjoin command can be executed.
1273
1274 This is the case if a layer is selected and that layer has a
1275 shapestore that has an original shapestore.
1276 """
1277 layer = context.mainwindow.SelectedLayer()
1278 if layer is None:
1279 return 0
1280 getstore = getattr(layer, "ShapeStore", None)
1281 if getstore is not None:
1282 return getstore().OrigShapeStore() is not None
1283 else:
1284 return 0
1285 _method_command("layer_unjointable", _("&Unjoin Table..."), "LayerUnjoinTable",
1286 sensitive = _can_unjoin,
1287 helptext = _("Undo the last join operation"))
1288
1289
1290 def _has_tables(context):
1291 return bool(context.session.Tables())
1292
1293 # Table menu
1294 _method_command("table_open", _("&Open..."), "TableOpen",
1295 helptext = _("Open a DBF-table from a file"))
1296 _method_command("table_close", _("&Close..."), "TableClose",
1297 sensitive = lambda context: bool(context.session.UnreferencedTables()),
1298 helptext = _("Close one or more tables from a list"))
1299 _method_command("table_rename", _("&Rename..."), "TableRename",
1300 sensitive = _has_tables,
1301 helptext = _("Rename one or more tables"))
1302 _method_command("table_show", _("&Show..."), "TableShow",
1303 sensitive = _has_tables,
1304 helptext = _("Show one or more tables in a dialog"))
1305 _method_command("table_join", _("&Join..."), "TableJoin",
1306 sensitive = _has_tables,
1307 helptext = _("Join two tables creating a new one"))
1308
1309 # Export only under Windows ...
1310 map_menu = ["layer_add", "layer_add_db", "rasterlayer_add", "layer_remove",
1311 None,
1312 "map_rename",
1313 "map_projection",
1314 None,
1315 "map_zoom_in_tool", "map_zoom_out_tool",
1316 "map_pan_tool",
1317 "map_full_extent",
1318 "layer_full_extent",
1319 "selected_full_extent",
1320 None,
1321 "map_identify_tool", "map_label_tool",
1322 None,
1323 "toggle_legend",
1324 None]
1325 if wx.Platform == '__WXMSW__':
1326 map_menu.append("map_export")
1327 map_menu.append("map_print")
1328
1329 # the menu structure
1330 main_menu = Menu("<main>", "<main>",
1331 [Menu("file", _("&File"),
1332 ["new_session", "open_session", None,
1333 "save_session", "save_session_as", None,
1334 "database_management", None,
1335 "toggle_session_tree", None,
1336 "exit"]),
1337 Menu("map", _("&Map"), map_menu),
1338 Menu("layer", _("&Layer"),
1339 ["layer_rename", "layer_duplicate",
1340 None,
1341 "layer_raise", "layer_lower",
1342 None,
1343 "layer_show", "layer_hide",
1344 None,
1345 "layer_projection",
1346 None,
1347 "layer_show_table",
1348 "layer_jointable",
1349 "layer_unjointable",
1350 None,
1351 "layer_properties"]),
1352 Menu("table", _("&Table"),
1353 ["table_open", "table_close", "table_rename",
1354 None,
1355 "table_show",
1356 None,
1357 "table_join"]),
1358 Menu("help", _("&Help"),
1359 ["help_about"])])
1360
1361 # the main toolbar
1362
1363 main_toolbar = Menu("<toolbar>", "<toolbar>",
1364 ["map_zoom_in_tool", "map_zoom_out_tool", "map_pan_tool",
1365 "map_full_extent",
1366 "layer_full_extent",
1367 "selected_full_extent",
1368 None,
1369 "map_identify_tool", "map_label_tool"])
1370

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26