14
__version__ = "$Revision: 1.128 $"
15
# $Source: /thubanrepository/thuban/Thuban/UI/mainwindow.py,v $
16
# $Id: mainwindow.py,v 1.128 2004/02/13 10:46:44 bh Exp $
14
__version__ = "$Revision: 2700 $"
16
# $Id: mainwindow.py 2700 2006-09-18 14:27:02Z dpinte $
21
from wxPython.wx import *
25
25
from Thuban import _
26
from Thuban.Model.messages import TITLE_CHANGED
26
from Thuban.Model.messages import TITLE_CHANGED, LAYER_PROJECTION_CHANGED, \
27
MAP_PROJECTION_CHANGED, MAP_LAYERS_ADDED, MAP_LAYERS_REMOVED
27
29
from Thuban.Model.session import create_empty_session
28
30
from Thuban.Model.layer import Layer, RasterLayer
29
31
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
32
from wx.lib.dialogs import MultipleChoiceDialog
37
36
import tableview, identifyview
38
from Thuban.UI.classifier import Classifier
40
38
from menu import Menu
73
80
"SelectedShapes": "canvas",
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,
76
89
def __init__(self, parent, ID, title, application, interactor,
77
initial_message = None, size = wxSize(-1, -1)):
78
DockFrame.__init__(self, parent, ID, title, wxDefaultPosition, size)
90
initial_message = None, size = wx.Size(-1, -1)):
91
DockFrame.__init__(self, parent, ID, title, wx.DefaultPosition, size)
79
92
#wxFrame.__init__(self, parent, ID, title, wxDefaultPosition, size)
81
94
self.application = application
175
190
"""Bind the necessary events for the given command and ID"""
176
191
if not self.events_bound.has_key(ID):
177
192
# the events haven't been bound yet
178
EVT_MENU(self, ID, self.invoke_command)
193
self.Bind(wx.EVT_MENU, self.invoke_command, id=ID)
179
194
if command.IsDynamic():
180
EVT_UPDATE_UI(self, ID, self.update_command_ui)
195
self.Bind(wx.EVT_UPDATE_UI, self.update_command_ui, id=ID)
182
197
def build_menu_bar(self, menudesc):
183
198
"""Build and return the menu bar from the menu description"""
184
menu_bar = wxMenuBar()
199
menu_bar = wx.MenuBar()
186
201
for item in menudesc.items:
187
202
# here the items must all be Menu instances themselves
214
229
The parameter should be an instance of the Menu class but it
215
230
should not contain submenus.
217
toolbar = self.CreateToolBar(wxTB_3DBUTTONS)
232
toolbar = self.CreateToolBar(wx.TB_3DBUTTONS)
219
234
# set the size of the tools' bitmaps. Not needed on wxGTK, but
220
235
# on Windows, although it doesn't work very well there. It seems
221
236
# that only 16x16 icons are really supported on windows.
222
237
# We probably shouldn't hardwire the bitmap size here.
223
toolbar.SetToolBitmapSize(wxSize(24, 24))
238
toolbar.SetToolBitmapSize(wx.Size(24, 24))
225
240
for item in toolbardesc.items:
302
317
if command.IsCheckCommand():
303
318
event.Check(command.Checked(context))
305
def RunMessageBox(self, title, text, flags = wxOK | wxICON_INFORMATION):
320
def RunMessageBox(self, title, text, flags = wx.OK | wx.ICON_INFORMATION):
306
321
"""Run a modal message box with the given text, title and flags
307
322
and return the result"""
308
dlg = wxMessageDialog(self, text, title, flags)
323
dlg = wx.MessageDialog(self, text, title, flags)
309
324
dlg.CenterOnParent()
310
325
result = dlg.ShowModal()
332
347
def get_open_dialog(self, name):
333
348
return self.dialogs.get(name)
335
def view_position_changed(self):
350
def update_status_bar(self, *args):
351
"""Handler for a bunch of messages that may require a status bar update
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.
357
These messages affect the text in the status bar in the following way:
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.
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
368
The text is displayed in the status bar using the
369
set_position_text method.
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.
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.
336
384
pos = self.canvas.CurrentPosition()
337
385
if pos is not None:
338
386
text = "(%10.10g, %10.10g)" % pos
388
for layer in self.canvas.Map().Layers():
389
bbox = layer.LatLongBoundingBox()
391
left, bottom, right, top = bbox
392
if not (-180 <= left <= 180 and
393
-180 <= right <= 180 and
395
-90 <= bottom <= 90):
396
text = _("Select layer '%s' and pick a projection "
397
"using Layer/Projection...") % layer.title
341
400
self.set_position_text(text)
343
402
def set_position_text(self, text):
344
"""Set the statusbar text showing the current position.
403
"""Set the statusbar text to that created by update_status_bar
346
405
By default the text is shown in field 0 of the status bar.
347
406
Override this method in derived classes to put it into a
348
407
different field of the statusbar.
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.
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.
350
417
self.SetStatusText(text)
419
def OpenOrRaiseDialog(self, name, dialog_class, *args, **kw):
421
Open or raise a dialog.
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.
427
dialog = self.get_open_dialog(name)
430
dialog = dialog_class(self, name, *args, **kw)
431
self.add_dialog(name, dialog)
352
436
def save_modified_session(self, can_veto = 1):
353
437
"""If the current session has been modified, ask the user
354
438
whether to save it and do so if requested. Return the outcome of
359
443
a cancel button, otherwise not.
361
445
if self.application.session.WasModified():
362
flags = wxYES_NO | wxICON_QUESTION
446
flags = wx.YES_NO | wx.ICON_QUESTION
364
flags = flags | wxCANCEL
448
flags = flags | wx.CANCEL
365
449
result = self.RunMessageBox(_("Exit"),
366
450
_("The session has been modified."
367
451
" Do you want to save it?"),
369
if result == wxID_YES:
453
if result == wx.ID_YES:
370
454
self.SaveSession()
375
459
def NewSession(self):
376
if self.save_modified_session() != wxID_CANCEL:
460
if self.save_modified_session() != wx.ID_CANCEL:
377
461
self.application.SetSession(create_empty_session())
379
463
def OpenSession(self):
380
if self.save_modified_session() != wxID_CANCEL:
381
dlg = wxFileDialog(self, _("Open Session"),
382
self.application.Path("data"), "",
383
"Thuban Session File (*.thuban)|*.thuban",
385
if dlg.ShowModal() == wxID_OK:
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",
469
if dlg.ShowModal() == wx.ID_OK:
386
470
self.application.OpenSession(dlg.GetPath(),
387
471
self.run_db_param_dialog)
388
472
self.application.SetPath("data", dlg.GetPath())
400
484
self.application.SaveSession()
402
486
def SaveSessionAs(self):
403
dlg = wxFileDialog(self, _("Save Session As"),
487
dlg = wx.FileDialog(self, _("Save Session As"),
404
488
self.application.Path("data"), "",
405
"Thuban Session File (*.thuban)|*.thuban",
406
wxSAVE|wxOVERWRITE_PROMPT)
407
if dlg.ShowModal() == wxID_OK:
489
"Thuban Session File (*.thuban)|*.thuban",
490
wx.SAVE|wx.OVERWRITE_PROMPT)
491
if dlg.ShowModal() == wx.ID_OK:
408
492
self.application.session.SetFilename(dlg.GetPath())
409
493
self.application.SaveSession()
410
494
self.application.SetPath("data",dlg.GetPath())
416
500
def OnClose(self, event):
417
501
result = self.save_modified_session(can_veto = event.CanVeto())
418
if result == wxID_CANCEL:
502
if result == wx.ID_CANCEL:
421
505
# FIXME: it would be better to tie the unsubscription to
422
506
# wx's destroy event, but that isn't implemented for wxGTK
424
self.canvas.Unsubscribe(VIEW_POSITION, self.view_position_changed)
508
for channel in self.update_status_bar_messages:
509
self.canvas.Unsubscribe(channel, self.update_status_bar)
425
511
DockFrame.OnClose(self, event)
426
512
for dlg in self.dialogs.values():
474
560
def AddLayer(self):
475
dlg = wxFileDialog(self, _("Select one or more data files"),
561
dlg = wx.FileDialog(self, _("Select one or more data files"),
476
562
self.application.Path("data"), "",
477
563
_("Shapefiles (*.shp)") + "|*.shp;*.SHP|" +
478
564
_("All Files (*.*)") + "|*.*",
480
if dlg.ShowModal() == wxID_OK:
565
wx.OPEN | wx.MULTIPLE)
566
if dlg.ShowModal() == wx.ID_OK:
481
567
filenames = dlg.GetPaths()
482
568
for filename in filenames:
483
569
title = os.path.splitext(os.path.basename(filename))[0]
528
614
session = self.application.Session()
529
615
dlg = ChooseDBTableDialog(self, self.application.Session())
531
if dlg.ShowModal() == wxID_OK:
532
dbconn, dbtable = dlg.GetTable()
617
if dlg.ShowModal() == wx.ID_OK:
618
dbconn, dbtable, id_column, geo_column = dlg.GetTable()
534
620
title = str(dbtable)
536
622
# Chose the correct Interface for the database type
537
store = PostGISShapeStore(dbconn, dbtable)
538
session.AddShapeStore(store)
623
store = session.OpenDBShapeStore(dbconn, dbtable,
624
id_column = id_column,
625
geometry_column = geo_column)
539
626
layer = Layer(title, store)
541
628
# Some error occured while initializing the layer
628
731
return layer is not None and hasattr(layer, "ShapeStore")
630
733
def LayerShowTable(self):
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.
631
740
layer = self.current_layer()
632
741
if layer is not None:
742
if not hasattr(layer, "ShapeStore"):
633
744
table = layer.ShapeStore().Table()
634
745
name = "table_view" + str(id(table))
635
746
dialog = self.get_open_dialog(name)
683
793
self.OpenLayerProperties(layer)
685
795
def OpenLayerProperties(self, layer, group = None):
686
name = "layer_properties" + str(id(layer))
687
dialog = self.get_open_dialog(name)
690
dialog = Classifier(self, name, self.Map(), layer, group)
691
self.add_dialog(name, dialog)
797
Open or raise the properties dialog.
799
This method opens or raises the properties dialog for the
800
currently selected layer if one is defined for this layer
803
dialog_class = layer_properties_dialogs.get(layer)
805
if dialog_class is not None:
806
name = "layer_properties" + str(id(layer))
807
self.OpenOrRaiseDialog(name, dialog_class, layer, group = group)
695
809
def LayerJoinTable(self):
696
810
layer = self.canvas.SelectedLayer()
728
842
def LegendShown(self):
729
843
"""Return true iff the legend is currently open"""
730
dialog = self.FindRegisteredDock("legend")
844
dialog = self.FindRegisteredDock("legend")
731
845
return dialog is not None and dialog.IsShown()
733
847
def TableOpen(self):
734
dlg = wxFileDialog(self, _("Open Table"),
735
self.application.Path("data"), "",
736
_("DBF Files (*.dbf)") + "|*.dbf|" +
848
dlg = wx.FileDialog(self, _("Open Table"),
849
self.application.Path("data"), "",
850
_("DBF Files (*.dbf)") + "|*.dbf|" +
737
851
#_("CSV Files (*.csv)") + "|*.csv|" +
738
_("All Files (*.*)") + "|*.*",
740
if dlg.ShowModal() == wxID_OK:
852
_("All Files (*.*)") + "|*.*",
854
if dlg.ShowModal() == wx.ID_OK:
741
855
filename = dlg.GetPath()
756
870
lst = [(t.Title(), t) for t in tables]
758
872
titles = [i[0] for i in lst]
759
dlg = wxMultipleChoiceDialog(self, _("Pick the tables to close:"),
873
dlg = MultipleChoiceDialog(self, _("Pick the tables to close:"),
760
874
_("Close Table"), titles,
761
875
size = (400, 300),
762
style = wxDEFAULT_DIALOG_STYLE |
764
if dlg.ShowModal() == wxID_OK:
876
style = wx.DEFAULT_DIALOG_STYLE |
878
if dlg.ShowModal() == wx.ID_OK:
765
879
for i in dlg.GetValue():
766
880
self.application.session.RemoveTable(lst[i][1])
777
891
lst = [(t.Title(), t) for t in tables]
779
893
titles = [i[0] for i in lst]
780
dlg = wxMultipleChoiceDialog(self, _("Pick the table to show:"),
894
dlg = MultipleChoiceDialog(self, _("Pick the table to show:"),
781
895
_("Show Table"), titles,
783
style = wxDEFAULT_DIALOG_STYLE |
785
if (dlg.ShowModal() == wxID_OK):
897
style = wx.DEFAULT_DIALOG_STYLE |
899
if (dlg.ShowModal() == wx.ID_OK):
786
900
for i in dlg.GetValue():
787
901
# XXX: if the table belongs to a layer, open a
788
902
# LayerTableFrame instead of QueryTableFrame
812
926
lst = [(t.Title(), t) for t in tables]
814
928
titles = [i[0] for i in lst]
815
dlg = wxMultipleChoiceDialog(self, _("Pick the table to rename:"),
929
dlg = MultipleChoiceDialog(self, _("Pick the table to rename:"),
816
930
_("Rename Table"), titles,
818
style = wxDEFAULT_DIALOG_STYLE |
820
if (dlg.ShowModal() == wxID_OK):
932
style = wx.DEFAULT_DIALOG_STYLE |
934
if (dlg.ShowModal() == wx.ID_OK):
821
935
to_rename = [lst[i][1] for i in dlg.GetValue()]
1134
1256
sensitive = _has_selected_shape_layer,
1135
1257
helptext = _("Join and attach a table to the selected layer"))
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)
1137
1271
def _can_unjoin(context):
1138
1272
"""Return whether the Layer/Unjoin command can be executed.