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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1620 - (hide annotations)
Wed Aug 20 13:14:22 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/mainwindow.py
File MIME type: text/x-python
File size: 44829 byte(s)
Add dialogs and commands to open database connections and add
database layers.

* Thuban/UI/mainwindow.py (MainWindow.DatabaseManagement): New
method to open the database connection management dialog
(MainWindow.AddDBLayer): New method to add a layer from a database
(_has_dbconnections): New helper function to use for sensitivity
(database_management command, layer_add_db command): New commands
that call the above new methods.
(main_menu): Add the new commands to the menu.

* Thuban/Model/postgisdb.py (PostGISConnection.__init__)
(PostGISConnection.connect): Establish the actual connection in a
separate method and call it in __init__. This makes it easier to
override the behavior in test cases
(PostGISConnection.BriefDescription): New method to return a brief
description for use in dialogs.

* test/test_postgis_db.py (NonConnection): DB connection that
doesn't actually connect
(TestBriefDescription): New class with tests for the new
BriefDescription method

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26