/[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 2365 - (show annotations)
Fri Oct 1 18:22:32 2004 UTC (20 years, 5 months ago) by joey
Original Path: trunk/thuban/Thuban/UI/mainwindow.py
File MIME type: text/x-python
File size: 50078 byte(s)
Utilise the new ClassMapper for global registration of properties
dialog classes (which are indeed layer-specific).

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26