chiark / gitweb /
Merge tag '15.01-RC4' into upstream
[cura.git] / Cura / gui / mainWindow.py
1 __copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"
2
3 import wx
4 import os
5 import webbrowser
6 import sys
7
8
9 from Cura.gui import configBase
10 from Cura.gui import expertConfig
11 from Cura.gui import alterationPanel
12 from Cura.gui import pluginPanel
13 from Cura.gui import preferencesDialog
14 from Cura.gui import configWizard
15 from Cura.gui import firmwareInstall
16 from Cura.gui import simpleMode
17 from Cura.gui import sceneView
18 from Cura.gui import aboutWindow
19 from Cura.gui.util import dropTarget
20 #from Cura.gui.tools import batchRun
21 from Cura.gui.tools import pidDebugger
22 from Cura.gui.tools import minecraftImport
23 from Cura.util import profile
24 from Cura.util import version
25 import platform
26 from Cura.util import meshLoader
27
28 try:
29         #MacOS release currently lacks some wx components, like the Publisher.
30         from wx.lib.pubsub import Publisher
31 except:
32         Publisher = None
33
34 class mainWindow(wx.Frame):
35         def __init__(self):
36                 super(mainWindow, self).__init__(None, title=_('Cura - ') + version.getVersion())
37
38                 wx.EVT_CLOSE(self, self.OnClose)
39
40                 # allow dropping any file, restrict later
41                 self.SetDropTarget(dropTarget.FileDropTarget(self.OnDropFiles))
42
43                 # TODO: wxWidgets 2.9.4 has a bug when NSView does not register for dragged types when wx drop target is set. It was fixed in 2.9.5
44                 if sys.platform.startswith('darwin'):
45                         try:
46                                 import objc
47                                 nswindow = objc.objc_object(c_void_p=self.MacGetTopLevelWindowRef())
48                                 view = nswindow.contentView()
49                                 view.registerForDraggedTypes_([u'NSFilenamesPboardType'])
50                         except:
51                                 pass
52
53                 self.normalModeOnlyItems = []
54
55                 mruFile = os.path.join(profile.getBasePath(), 'mru_filelist.ini')
56                 self.config = wx.FileConfig(appName="Cura",
57                                                 localFilename=mruFile,
58                                                 style=wx.CONFIG_USE_LOCAL_FILE)
59
60                 self.ID_MRU_MODEL1, self.ID_MRU_MODEL2, self.ID_MRU_MODEL3, self.ID_MRU_MODEL4, self.ID_MRU_MODEL5, self.ID_MRU_MODEL6, self.ID_MRU_MODEL7, self.ID_MRU_MODEL8, self.ID_MRU_MODEL9, self.ID_MRU_MODEL10 = [wx.NewId() for line in xrange(10)]
61                 self.modelFileHistory = wx.FileHistory(10, self.ID_MRU_MODEL1)
62                 self.config.SetPath("/ModelMRU")
63                 self.modelFileHistory.Load(self.config)
64
65                 self.ID_MRU_PROFILE1, self.ID_MRU_PROFILE2, self.ID_MRU_PROFILE3, self.ID_MRU_PROFILE4, self.ID_MRU_PROFILE5, self.ID_MRU_PROFILE6, self.ID_MRU_PROFILE7, self.ID_MRU_PROFILE8, self.ID_MRU_PROFILE9, self.ID_MRU_PROFILE10 = [wx.NewId() for line in xrange(10)]
66                 self.profileFileHistory = wx.FileHistory(10, self.ID_MRU_PROFILE1)
67                 self.config.SetPath("/ProfileMRU")
68                 self.profileFileHistory.Load(self.config)
69
70                 self.menubar = wx.MenuBar()
71                 self.fileMenu = wx.Menu()
72                 i = self.fileMenu.Append(-1, _("Load model file...\tCTRL+L"))
73                 self.Bind(wx.EVT_MENU, lambda e: self.scene.showLoadModel(), i)
74                 i = self.fileMenu.Append(-1, _("Save model...\tCTRL+S"))
75                 self.Bind(wx.EVT_MENU, lambda e: self.scene.showSaveModel(), i)
76                 i = self.fileMenu.Append(-1, _("Reload platform\tF5"))
77                 self.Bind(wx.EVT_MENU, lambda e: self.scene.reloadScene(e), i)
78                 i = self.fileMenu.Append(-1, _("Clear platform"))
79                 self.Bind(wx.EVT_MENU, lambda e: self.scene.OnDeleteAll(e), i)
80
81                 self.fileMenu.AppendSeparator()
82                 i = self.fileMenu.Append(-1, _("Print...\tCTRL+P"))
83                 self.Bind(wx.EVT_MENU, lambda e: self.scene.OnPrintButton(1), i)
84                 i = self.fileMenu.Append(-1, _("Save GCode..."))
85                 self.Bind(wx.EVT_MENU, lambda e: self.scene.showSaveGCode(), i)
86                 i = self.fileMenu.Append(-1, _("Show slice engine log..."))
87                 self.Bind(wx.EVT_MENU, lambda e: self.scene._showEngineLog(), i)
88
89                 self.fileMenu.AppendSeparator()
90                 i = self.fileMenu.Append(-1, _("Open Profile..."))
91                 self.normalModeOnlyItems.append(i)
92                 self.Bind(wx.EVT_MENU, self.OnLoadProfile, i)
93                 i = self.fileMenu.Append(-1, _("Save Profile..."))
94                 self.normalModeOnlyItems.append(i)
95                 self.Bind(wx.EVT_MENU, self.OnSaveProfile, i)
96                 if version.isDevVersion():
97                         i = self.fileMenu.Append(-1, "Save difference from default...")
98                         self.normalModeOnlyItems.append(i)
99                         self.Bind(wx.EVT_MENU, self.OnSaveDifferences, i)
100                 i = self.fileMenu.Append(-1, _("Load Profile from GCode..."))
101                 self.normalModeOnlyItems.append(i)
102                 self.Bind(wx.EVT_MENU, self.OnLoadProfileFromGcode, i)
103                 self.fileMenu.AppendSeparator()
104                 i = self.fileMenu.Append(-1, _("Reset Profile to default"))
105                 self.normalModeOnlyItems.append(i)
106                 self.Bind(wx.EVT_MENU, self.OnResetProfile, i)
107
108                 self.fileMenu.AppendSeparator()
109                 i = self.fileMenu.Append(-1, _("Preferences...\tCTRL+,"))
110                 self.Bind(wx.EVT_MENU, self.OnPreferences, i)
111                 i = self.fileMenu.Append(-1, _("Machine settings..."))
112                 self.Bind(wx.EVT_MENU, self.OnMachineSettings, i)
113                 self.fileMenu.AppendSeparator()
114
115                 # Model MRU list
116                 modelHistoryMenu = wx.Menu()
117                 self.fileMenu.AppendMenu(wx.NewId(), '&' + _("Recent Model Files"), modelHistoryMenu)
118                 self.modelFileHistory.UseMenu(modelHistoryMenu)
119                 self.modelFileHistory.AddFilesToMenu()
120                 self.Bind(wx.EVT_MENU_RANGE, self.OnModelMRU, id=self.ID_MRU_MODEL1, id2=self.ID_MRU_MODEL10)
121
122                 # Profle MRU list
123                 profileHistoryMenu = wx.Menu()
124                 self.fileMenu.AppendMenu(wx.NewId(), _("Recent Profile Files"), profileHistoryMenu)
125                 self.profileFileHistory.UseMenu(profileHistoryMenu)
126                 self.profileFileHistory.AddFilesToMenu()
127                 self.Bind(wx.EVT_MENU_RANGE, self.OnProfileMRU, id=self.ID_MRU_PROFILE1, id2=self.ID_MRU_PROFILE10)
128
129                 self.fileMenu.AppendSeparator()
130                 i = self.fileMenu.Append(wx.ID_EXIT, _("Quit"))
131                 self.Bind(wx.EVT_MENU, self.OnQuit, i)
132                 self.menubar.Append(self.fileMenu, '&' + _("File"))
133
134                 toolsMenu = wx.Menu()
135                 #i = toolsMenu.Append(-1, 'Batch run...')
136                 #self.Bind(wx.EVT_MENU, self.OnBatchRun, i)
137                 #self.normalModeOnlyItems.append(i)
138
139                 if minecraftImport.hasMinecraft():
140                         i = toolsMenu.Append(-1, _("Minecraft map import..."))
141                         self.Bind(wx.EVT_MENU, self.OnMinecraftImport, i)
142
143                 if version.isDevVersion():
144                         i = toolsMenu.Append(-1, _("PID Debugger..."))
145                         self.Bind(wx.EVT_MENU, self.OnPIDDebugger, i)
146                         i = toolsMenu.Append(-1, _("Auto Firmware Update..."))
147                         self.Bind(wx.EVT_MENU, self.OnAutoFirmwareUpdate, i)
148
149                 i = toolsMenu.Append(-1, _("Copy profile to clipboard"))
150                 self.Bind(wx.EVT_MENU, self.onCopyProfileClipboard,i)
151
152                 toolsMenu.AppendSeparator()
153                 self.allAtOnceItem = toolsMenu.Append(-1, _("Print all at once"), kind=wx.ITEM_RADIO)
154                 self.Bind(wx.EVT_MENU, self.onOneAtATimeSwitch, self.allAtOnceItem)
155                 self.oneAtATime = toolsMenu.Append(-1, _("Print one at a time"), kind=wx.ITEM_RADIO)
156                 self.Bind(wx.EVT_MENU, self.onOneAtATimeSwitch, self.oneAtATime)
157                 if profile.getPreference('oneAtATime') == 'True':
158                         self.oneAtATime.Check(True)
159                 else:
160                         self.allAtOnceItem.Check(True)
161
162                 self.menubar.Append(toolsMenu, _("Tools"))
163
164                 #Machine menu for machine configuration/tooling
165                 self.machineMenu = wx.Menu()
166                 self.updateMachineMenu()
167
168                 self.menubar.Append(self.machineMenu, _("Machine"))
169
170                 expertMenu = wx.Menu()
171                 i = expertMenu.Append(-1, _("Switch to quickprint..."), kind=wx.ITEM_RADIO)
172                 self.switchToQuickprintMenuItem = i
173                 self.Bind(wx.EVT_MENU, self.OnSimpleSwitch, i)
174
175                 i = expertMenu.Append(-1, _("Switch to full settings..."), kind=wx.ITEM_RADIO)
176                 self.switchToNormalMenuItem = i
177                 self.Bind(wx.EVT_MENU, self.OnNormalSwitch, i)
178                 expertMenu.AppendSeparator()
179
180                 i = expertMenu.Append(-1, _("Open expert settings...\tCTRL+E"))
181                 self.normalModeOnlyItems.append(i)
182                 self.Bind(wx.EVT_MENU, self.OnExpertOpen, i)
183                 expertMenu.AppendSeparator()
184                 self.bedLevelWizardMenuItem = expertMenu.Append(-1, _("Run bed leveling wizard..."))
185                 self.Bind(wx.EVT_MENU, self.OnBedLevelWizard, self.bedLevelWizardMenuItem)
186                 self.headOffsetWizardMenuItem = expertMenu.Append(-1, _("Run head offset wizard..."))
187                 self.Bind(wx.EVT_MENU, self.OnHeadOffsetWizard, self.headOffsetWizardMenuItem)
188
189                 self.menubar.Append(expertMenu, _("Expert"))
190
191                 helpMenu = wx.Menu()
192                 i = helpMenu.Append(-1, _("Online documentation..."))
193                 self.Bind(wx.EVT_MENU, lambda e: webbrowser.open('http://lulzbot.com/cura'), i)
194                 i = helpMenu.Append(-1, _("Report a problem..."))
195                 self.Bind(wx.EVT_MENU, lambda e: webbrowser.open('https://github.com/alephobjects/Cura/issues'), i)
196                 #i = helpMenu.Append(-1, _("Check for update..."))
197                 #self.Bind(wx.EVT_MENU, self.OnCheckForUpdate, i)
198                 #i = helpMenu.Append(-1, _("Open YouMagine website..."))
199                 #self.Bind(wx.EVT_MENU, lambda e: webbrowser.open('https://www.youmagine.com/'), i)
200                 i = helpMenu.Append(-1, _("About Cura..."))
201                 self.Bind(wx.EVT_MENU, self.OnAbout, i)
202                 self.menubar.Append(helpMenu, _("Help"))
203                 self.SetMenuBar(self.menubar)
204
205                 self.splitter = wx.SplitterWindow(self, style = wx.SP_3D | wx.SP_LIVE_UPDATE)
206                 self.leftPane = wx.Panel(self.splitter, style=wx.BORDER_NONE)
207                 self.rightPane = wx.Panel(self.splitter, style=wx.BORDER_NONE)
208                 self.splitter.Bind(wx.EVT_SPLITTER_DCLICK, lambda evt: evt.Veto())
209
210                 #Preview window
211                 self.scene = sceneView.SceneView(self.rightPane)
212
213                 ##Gui components##
214                 self.simpleSettingsPanel = simpleMode.simpleModePanel(self.leftPane, self.scene.sceneUpdated)
215                 self.normalSettingsPanel = normalSettingsPanel(self.leftPane, self.scene.sceneUpdated)
216
217                 self.leftSizer = wx.BoxSizer(wx.VERTICAL)
218                 self.leftSizer.Add(self.simpleSettingsPanel, 1)
219                 self.leftSizer.Add(self.normalSettingsPanel, 1, wx.EXPAND)
220                 self.leftPane.SetSizer(self.leftSizer)
221
222                 #Main sizer, to position the preview window, buttons and tab control
223                 sizer = wx.BoxSizer()
224                 self.rightPane.SetSizer(sizer)
225                 sizer.Add(self.scene, 1, flag=wx.EXPAND)
226
227                 # Main window sizer
228                 sizer = wx.BoxSizer(wx.VERTICAL)
229                 self.SetSizer(sizer)
230                 sizer.Add(self.splitter, 1, wx.EXPAND)
231                 sizer.Layout()
232                 self.sizer = sizer
233
234                 self.updateProfileToAllControls()
235
236                 self.SetBackgroundColour(self.normalSettingsPanel.GetBackgroundColour())
237
238                 self.simpleSettingsPanel.Show(False)
239                 self.normalSettingsPanel.Show(False)
240
241                 # Set default window size & position
242                 self.SetSize((wx.Display().GetClientArea().GetWidth()/2,wx.Display().GetClientArea().GetHeight()/2))
243                 self.Centre()
244
245                 #Timer set; used to check if profile is on the clipboard
246                 self.timer = wx.Timer(self)
247                 self.Bind(wx.EVT_TIMER, self.onTimer)
248                 self.timer.Start(1000)
249                 self.lastTriedClipboard = profile.getProfileString()
250
251                 # Restore the window position, size & state from the preferences file
252                 try:
253                         if profile.getPreference('window_maximized') == 'True':
254                                 self.Maximize(True)
255                         else:
256                                 posx = int(profile.getPreference('window_pos_x'))
257                                 posy = int(profile.getPreference('window_pos_y'))
258                                 width = int(profile.getPreference('window_width'))
259                                 height = int(profile.getPreference('window_height'))
260                                 if posx > 0 or posy > 0:
261                                         self.SetPosition((posx,posy))
262                                 if width > 0 and height > 0:
263                                         self.SetSize((width,height))
264
265                         self.normalSashPos = int(profile.getPreference('window_normal_sash'))
266                 except:
267                         self.normalSashPos = 0
268                         self.Maximize(True)
269                 if self.normalSashPos < self.normalSettingsPanel.printPanel.GetBestSize()[0] + 5:
270                         self.normalSashPos = self.normalSettingsPanel.printPanel.GetBestSize()[0] + 5
271
272                 self.splitter.SplitVertically(self.leftPane, self.rightPane, self.normalSashPos)
273
274                 if wx.Display.GetFromPoint(self.GetPosition()) < 0:
275                         self.Centre()
276                 if wx.Display.GetFromPoint((self.GetPositionTuple()[0] + self.GetSizeTuple()[1], self.GetPositionTuple()[1] + self.GetSizeTuple()[1])) < 0:
277                         self.Centre()
278                 if wx.Display.GetFromPoint(self.GetPosition()) < 0:
279                         self.SetSize((800,600))
280                         self.Centre()
281
282                 self.updateSliceMode()
283                 self.scene.SetFocus()
284                 self.dialogframe = None
285                 if Publisher is not None:
286                         Publisher().subscribe(self.onPluginUpdate, "pluginupdate")
287
288         def onPluginUpdate(self,msg): #receives commands from the plugin thread
289                 cmd = str(msg.data).split(";")
290                 if cmd[0] == "OpenPluginProgressWindow":
291                         if len(cmd)==1: #no titel received
292                                 cmd.append("Plugin")
293                         if len(cmd)<3: #no message text received
294                                 cmd.append("Plugin is executed...")
295                         dialogwidth = 300
296                         dialogheight = 80
297                         self.dialogframe = wx.Frame(self, -1, cmd[1],pos = ((wx.SystemSettings.GetMetric(wx.SYS_SCREEN_X)-dialogwidth)/2,(wx.SystemSettings.GetMetric(wx.SYS_SCREEN_Y)-dialogheight)/2), size=(dialogwidth,dialogheight), style = wx.STAY_ON_TOP)
298                         self.dialogpanel = wx.Panel(self.dialogframe, -1, pos = (0,0), size = (dialogwidth,dialogheight))
299                         self.dlgtext = wx.StaticText(self.dialogpanel, label = cmd[2], pos = (10,10), size = (280,40))
300                         self.dlgbar = wx.Gauge(self.dialogpanel,-1, 100, pos = (10,50), size = (280,20), style = wx.GA_HORIZONTAL)
301                         self.dialogframe.Show()
302
303                 elif cmd[0] == "Progress":
304                         number = int(cmd[1])
305                         if number <= 100 and self.dialogframe is not None:
306                                 self.dlgbar.SetValue(number)
307                         else:
308                                 self.dlgbar.SetValue(100)
309                 elif cmd[0] == "ClosePluginProgressWindow":
310                         self.dialogframe.Destroy()
311                         self.dialogframe=None
312                 else:
313                         print "Unknown Plugin update received: " + cmd[0]
314
315         def onTimer(self, e):
316                 #Check if there is something in the clipboard
317                 profileString = ""
318                 try:
319                         if not wx.TheClipboard.IsOpened():
320                                 if not wx.TheClipboard.Open():
321                                         return
322                                 do = wx.TextDataObject()
323                                 if wx.TheClipboard.GetData(do):
324                                         profileString = do.GetText()
325                                 wx.TheClipboard.Close()
326
327                                 startTag = "CURA_PROFILE_STRING:"
328                                 if startTag in profileString:
329                                         #print "Found correct syntax on clipboard"
330                                         profileString = profileString.replace("\n","").strip()
331                                         profileString = profileString[profileString.find(startTag)+len(startTag):]
332                                         if profileString != self.lastTriedClipboard:
333                                                 print profileString
334                                                 self.lastTriedClipboard = profileString
335                                                 profile.setProfileFromString(profileString)
336                                                 self.scene.notification.message(_("Loaded new profile from clipboard."))
337                                                 self.updateProfileToAllControls()
338                 except:
339                         print "Unable to read from clipboard"
340
341
342         def updateSliceMode(self, changedMode = True):
343                 isSimple = profile.getPreference('startMode') == 'Simple'
344
345                 self.normalSettingsPanel.Show(not isSimple)
346                 self.simpleSettingsPanel.Show(isSimple)
347                 self.leftPane.Layout()
348
349                 for i in self.normalModeOnlyItems:
350                         i.Enable(not isSimple)
351                 if isSimple:
352                         self.switchToQuickprintMenuItem.Check()
353                 else:
354                         self.switchToNormalMenuItem.Check()
355
356                 # Set splitter sash position & size
357                 if isSimple:
358                         # Save normal mode sash (only if we changed mode from normal
359                         # to simple)
360                         if changedMode:
361                                 self.normalSashPos = self.splitter.GetSashPosition()
362
363                         # Change location of sash to width of quick mode pane
364                         (width, height) = self.simpleSettingsPanel.GetSizer().GetSize()
365                         self.splitter.SetSashPosition(width, True)
366
367                         # Disable sash
368                         self.splitter.SetSashSize(0)
369                 else:
370                         # Only change the sash position if we changed mode from simple
371                         if changedMode:
372                                 self.splitter.SetSashPosition(self.normalSashPos, True)
373                         # Enabled sash
374                         self.splitter.SetSashSize(4)
375                 self.defaultFirmwareInstallMenuItem.Enable(firmwareInstall.getDefaultFirmware() is not None)
376                 if profile.getMachineSetting('machine_type') == 'ultimaker2' or profile.getMachineSetting('machine_type') == 'lulzbot_mini' or profile.getMachineSetting('machine_type') == 'lulzbot_TAZ':
377                         self.bedLevelWizardMenuItem.Enable(False)
378                         self.headOffsetWizardMenuItem.Enable(False)
379                 else:
380                         self.bedLevelWizardMenuItem.Enable(True)
381                         self.headOffsetWizardMenuItem.Enable(False)
382                 if int(profile.getMachineSetting('extruder_amount')) < 2:
383                         self.headOffsetWizardMenuItem.Enable(False)
384                 self.scene.updateProfileToControls()
385                 self.scene._scene.pushFree()
386
387         def onOneAtATimeSwitch(self, e):
388                 profile.putPreference('oneAtATime', self.oneAtATime.IsChecked())
389                 if self.oneAtATime.IsChecked() and profile.getMachineSettingFloat('extruder_head_size_height') < 1:
390                         wx.MessageBox(_('For "One at a time" printing, you need to have entered the correct head size and gantry height in the machine settings'), _('One at a time warning'), wx.OK | wx.ICON_WARNING)
391                 self.scene.updateProfileToControls()
392                 self.scene._scene.pushFree()
393                 self.scene.sceneUpdated()
394
395         def OnPreferences(self, e):
396                 prefDialog = preferencesDialog.preferencesDialog(self)
397                 prefDialog.Centre()
398                 prefDialog.Show()
399                 prefDialog.Raise()
400
401         def OnMachineSettings(self, e):
402                 prefDialog = preferencesDialog.machineSettingsDialog(self)
403                 prefDialog.Centre()
404                 prefDialog.Show()
405                 prefDialog.Raise()
406
407         def OnDropFiles(self, files):
408                 self.scene.loadFiles(files)
409
410         def OnModelMRU(self, e):
411                 fileNum = e.GetId() - self.ID_MRU_MODEL1
412                 path = self.modelFileHistory.GetHistoryFile(fileNum)
413                 # Update Model MRU
414                 self.modelFileHistory.AddFileToHistory(path)  # move up the list
415                 self.config.SetPath("/ModelMRU")
416                 self.modelFileHistory.Save(self.config)
417                 self.config.Flush()
418                 # Load Model
419                 profile.putPreference('lastFile', path)
420                 filelist = [ path ]
421                 self.scene.loadFiles(filelist)
422
423         def addToModelMRU(self, file):
424                 self.modelFileHistory.AddFileToHistory(file)
425                 self.config.SetPath("/ModelMRU")
426                 self.modelFileHistory.Save(self.config)
427                 self.config.Flush()
428
429         def OnProfileMRU(self, e):
430                 fileNum = e.GetId() - self.ID_MRU_PROFILE1
431                 path = self.profileFileHistory.GetHistoryFile(fileNum)
432                 # Update Profile MRU
433                 self.profileFileHistory.AddFileToHistory(path)  # move up the list
434                 self.config.SetPath("/ProfileMRU")
435                 self.profileFileHistory.Save(self.config)
436                 self.config.Flush()
437                 # Load Profile
438                 profile.loadProfile(path)
439                 self.updateProfileToAllControls()
440
441         def addToProfileMRU(self, file):
442                 self.profileFileHistory.AddFileToHistory(file)
443                 self.config.SetPath("/ProfileMRU")
444                 self.profileFileHistory.Save(self.config)
445                 self.config.Flush()
446
447         def updateProfileToAllControls(self):
448                 self.scene.updateProfileToControls()
449                 self.normalSettingsPanel.updateProfileToControls()
450                 self.simpleSettingsPanel.updateProfileToControls()
451
452         def reloadSettingPanels(self, changedSliceMode = False):
453                 self.leftSizer.Detach(self.simpleSettingsPanel)
454                 self.leftSizer.Detach(self.normalSettingsPanel)
455                 self.simpleSettingsPanel.Destroy()
456                 self.normalSettingsPanel.Destroy()
457                 self.simpleSettingsPanel = simpleMode.simpleModePanel(self.leftPane, lambda : self.scene.sceneUpdated())
458                 self.normalSettingsPanel = normalSettingsPanel(self.leftPane, lambda : self.scene.sceneUpdated())
459                 self.leftSizer.Add(self.simpleSettingsPanel, 1)
460                 self.leftSizer.Add(self.normalSettingsPanel, 1, wx.EXPAND)
461                 self.updateSliceMode(changedSliceMode)
462                 self.updateProfileToAllControls()
463
464         def updateMachineMenu(self):
465                 #Remove all items so we can rebuild the menu. Inserting items seems to cause crashes, so this is the safest way.
466                 for item in self.machineMenu.GetMenuItems():
467                         self.machineMenu.RemoveItem(item)
468
469                 #Add a menu item for each machine configuration.
470                 for n in xrange(0, profile.getMachineCount()):
471                         i = self.machineMenu.Append(n + 0x1000, profile.getMachineSetting('machine_name', n).title(), kind=wx.ITEM_RADIO)
472                         if n == int(profile.getPreferenceFloat('active_machine')):
473                                 i.Check(True)
474                         self.Bind(wx.EVT_MENU, lambda e: self.OnSelectMachine(e.GetId() - 0x1000), i)
475
476                 self.machineMenu.AppendSeparator()
477                 i = self.machineMenu.Append(-1, _("Add new machine..."))
478                 self.Bind(wx.EVT_MENU, self.OnAddNewMachine, i)
479                 i = self.machineMenu.Append(-1, _("Machine settings..."))
480                 self.Bind(wx.EVT_MENU, self.OnMachineSettings, i)
481
482                 #Add tools for machines.
483                 self.machineMenu.AppendSeparator()
484
485                 self.defaultFirmwareInstallMenuItem = self.machineMenu.Append(-1, _("Install default firmware..."))
486                 self.Bind(wx.EVT_MENU, self.OnDefaultMarlinFirmware, self.defaultFirmwareInstallMenuItem)
487
488                 i = self.machineMenu.Append(-1, _("Install custom firmware..."))
489                 self.Bind(wx.EVT_MENU, self.OnCustomFirmware, i)
490
491         def OnLoadProfile(self, e):
492                 dlg=wx.FileDialog(self, _("Select profile file to load"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
493                 dlg.SetWildcard("ini files (*.ini)|*.ini")
494                 if dlg.ShowModal() == wx.ID_OK:
495                         profileFile = dlg.GetPath()
496                         profile.loadProfile(profileFile)
497                         self.updateProfileToAllControls()
498
499                         # Update the Profile MRU
500                         self.addToProfileMRU(profileFile)
501                 dlg.Destroy()
502
503         def OnLoadProfileFromGcode(self, e):
504                 dlg=wx.FileDialog(self, _("Select gcode file to load profile from"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
505                 dlg.SetWildcard("gcode files (*%s)|*%s;*%s" % (profile.getGCodeExtension(), profile.getGCodeExtension(), profile.getGCodeExtension()[0:2]))
506                 if dlg.ShowModal() == wx.ID_OK:
507                         gcodeFile = dlg.GetPath()
508                         f = open(gcodeFile, 'r')
509                         hasProfile = False
510                         for line in f:
511                                 if line.startswith(';CURA_PROFILE_STRING:'):
512                                         profile.setProfileFromString(line[line.find(':')+1:].strip())
513                                         if ';{profile_string}' not in profile.getAlterationFile('end.gcode'):
514                                                 profile.setAlterationFile('end.gcode', profile.getAlterationFile('end.gcode') + '\n;{profile_string}')
515                                         hasProfile = True
516                         if hasProfile:
517                                 self.updateProfileToAllControls()
518                         else:
519                                 wx.MessageBox(_("No profile found in GCode file.\nThis feature only works with GCode files made by Cura 12.07 or newer."), _("Profile load error"), wx.OK | wx.ICON_INFORMATION)
520                 dlg.Destroy()
521
522         def OnSaveProfile(self, e):
523                 dlg=wx.FileDialog(self, _("Select profile file to save"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
524                 dlg.SetWildcard("ini files (*.ini)|*.ini")
525                 if dlg.ShowModal() == wx.ID_OK:
526                         profile_filename = dlg.GetPath()
527                         if not profile_filename.lower().endswith('.ini'): #hack for linux, as for some reason the .ini is not appended.
528                                 profile_filename += '.ini'
529                         profile.saveProfile(profile_filename)
530                 dlg.Destroy()
531
532         def OnSaveDifferences(self, e):
533                 dlg=wx.FileDialog(self, _("Select profile file to save"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
534                 dlg.SetWildcard("ini files (*.ini)|*.ini")
535                 if dlg.ShowModal() == wx.ID_OK:
536                         profile_filename = dlg.GetPath()
537                         if not profile_filename.lower().endswith('.ini'): #hack for linux, as for some reason the .ini is not appended.
538                                 profile_filename += '.ini'
539                         profile.saveProfileDifferenceFromDefault(profile_filename)
540                 dlg.Destroy()
541
542         def OnResetProfile(self, e):
543                 dlg = wx.MessageDialog(self, _("This will reset all profile settings to defaults.\nUnless you have saved your current profile, all settings will be lost!\nDo you really want to reset?"), _("Profile reset"), wx.YES_NO | wx.ICON_QUESTION)
544                 result = dlg.ShowModal() == wx.ID_YES
545                 dlg.Destroy()
546                 if result:
547                         profile.resetProfile()
548                         self.updateProfileToAllControls()
549
550         def OnSimpleSwitch(self, e):
551                 profile.putPreference('startMode', 'Simple')
552                 self.updateSliceMode()
553
554         def OnNormalSwitch(self, e):
555                 profile.putPreference('startMode', 'Normal')
556                 dlg = wx.MessageDialog(self, _("Copy the settings from quickprint to your full settings?\n(This will overwrite any full setting modifications you have)"), _("Profile copy"), wx.YES_NO | wx.ICON_QUESTION)
557                 result = dlg.ShowModal() == wx.ID_YES
558                 dlg.Destroy()
559                 if result:
560                         profile.resetProfile()
561                         for k, v in self.simpleSettingsPanel.getSettingOverrides().items():
562                                 profile.putProfileSetting(k, v)
563                         self.updateProfileToAllControls()
564                 self.updateSliceMode()
565
566         def OnDefaultMarlinFirmware(self, e):
567                 firmwareInstall.InstallFirmware(self)
568
569         def OnCustomFirmware(self, e):
570                 if profile.getMachineSetting('machine_type').startswith('ultimaker'):
571                         wx.MessageBox(_("Warning: Installing a custom firmware does not guarantee that you machine will function correctly, and could damage your machine."), _("Firmware update"), wx.OK | wx.ICON_EXCLAMATION)
572                 dlg=wx.FileDialog(self, _("Open firmware to upload"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
573                 dlg.SetWildcard("HEX file (*.hex)|*.hex;*.HEX")
574                 if dlg.ShowModal() == wx.ID_OK:
575                         filename = dlg.GetPath()
576                         dlg.Destroy()
577                         if not(os.path.exists(filename)):
578                                 return
579                         #For some reason my Ubuntu 10.10 crashes here.
580                         firmwareInstall.InstallFirmware(self, filename)
581
582         def OnAddNewMachine(self, e):
583                 self.Hide()
584                 wasSimple = profile.getPreference('startMode') == 'Simple'
585                 configWizard.ConfigWizard(True)
586                 isSimple = profile.getPreference('startMode') == 'Simple'
587                 self.Show()
588                 self.reloadSettingPanels(isSimple != wasSimple)
589                 self.updateMachineMenu()
590
591         def OnSelectMachine(self, index):
592                 profile.setActiveMachine(index)
593                 self.reloadSettingPanels(False)
594
595         def OnBedLevelWizard(self, e):
596                 configWizard.bedLevelWizard()
597
598         def OnHeadOffsetWizard(self, e):
599                 configWizard.headOffsetWizard()
600
601         def OnExpertOpen(self, e):
602                 ecw = expertConfig.expertConfigWindow(lambda : self.scene.sceneUpdated())
603                 ecw.Centre()
604                 ecw.Show()
605
606         def OnMinecraftImport(self, e):
607                 mi = minecraftImport.minecraftImportWindow(self)
608                 mi.Centre()
609                 mi.Show(True)
610
611         def OnPIDDebugger(self, e):
612                 debugger = pidDebugger.debuggerWindow(self)
613                 debugger.Centre()
614                 debugger.Show(True)
615
616         def OnAutoFirmwareUpdate(self, e):
617                 dlg=wx.FileDialog(self, _("Open firmware to upload"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
618                 dlg.SetWildcard("HEX file (*.hex)|*.hex;*.HEX")
619                 if dlg.ShowModal() == wx.ID_OK:
620                         filename = dlg.GetPath()
621                         dlg.Destroy()
622                         if not(os.path.exists(filename)):
623                                 return
624                         #For some reason my Ubuntu 10.10 crashes here.
625                         installer = firmwareInstall.AutoUpdateFirmware(self, filename)
626
627         def onCopyProfileClipboard(self, e):
628                 try:
629                         if not wx.TheClipboard.IsOpened():
630                                 wx.TheClipboard.Open()
631                                 clipData = wx.TextDataObject()
632                                 self.lastTriedClipboard = profile.getProfileString()
633                                 profileString = profile.insertNewlines("CURA_PROFILE_STRING:" + self.lastTriedClipboard)
634                                 clipData.SetText(profileString)
635                                 wx.TheClipboard.SetData(clipData)
636                                 wx.TheClipboard.Close()
637                 except:
638                         print "Could not write to clipboard, unable to get ownership. Another program is using the clipboard."
639
640         def OnCheckForUpdate(self, e):
641                 newVersion = version.checkForNewerVersion()
642                 if newVersion is not None:
643                         if wx.MessageBox(_("A new version of Cura is available, would you like to download?"), _("New version available"), wx.YES_NO | wx.ICON_INFORMATION) == wx.YES:
644                                 webbrowser.open(newVersion)
645                 else:
646                         wx.MessageBox(_("You are running the latest version of Cura!"), _("Awesome!"), wx.ICON_INFORMATION)
647
648         def OnAbout(self, e):
649                 aboutBox = aboutWindow.aboutWindow(self)
650                 aboutBox.Centre()
651                 aboutBox.Show()
652                 aboutBox.Raise()
653
654         def OnClose(self, e):
655                 profile.saveProfile(profile.getDefaultProfilePath(), True)
656
657                 # Save the window position, size & state from the preferences file
658                 profile.putPreference('window_maximized', self.IsMaximized())
659                 if not self.IsMaximized() and not self.IsIconized():
660                         (posx, posy) = self.GetPosition()
661                         profile.putPreference('window_pos_x', posx)
662                         profile.putPreference('window_pos_y', posy)
663                         (width, height) = self.GetSize()
664                         profile.putPreference('window_width', width)
665                         profile.putPreference('window_height', height)
666
667                         # Save normal sash position.  If in normal mode (!simple mode), get last position of sash before saving it...
668                         isSimple = profile.getPreference('startMode') == 'Simple'
669                         if not isSimple:
670                                 self.normalSashPos = self.splitter.GetSashPosition()
671                         profile.putPreference('window_normal_sash', self.normalSashPos)
672
673                 #HACK: Set the paint function of the glCanvas to nothing so it won't keep refreshing. Which can keep wxWidgets from quiting.
674                 print "Closing down"
675                 self.scene.OnPaint = lambda e : e
676                 self.scene.cleanup()
677                 self.Destroy()
678
679         def OnQuit(self, e):
680                 self.Close()
681
682 class normalSettingsPanel(configBase.configPanelBase):
683         "Main user interface window"
684         def __init__(self, parent, callback = None):
685                 super(normalSettingsPanel, self).__init__(parent, callback)
686
687                 #Main tabs
688                 self.nb = wx.Notebook(self)
689                 self.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
690                 self.GetSizer().Add(self.nb, 1, wx.EXPAND)
691
692                 (left, right, self.printPanel) = self.CreateDynamicConfigTab(self.nb, _('Basic'))
693                 self._addSettingsToPanels('basic', left, right)
694                 self.SizeLabelWidths(left, right)
695
696                 (left, right, self.advancedPanel) = self.CreateDynamicConfigTab(self.nb, _('Advanced'))
697                 self._addSettingsToPanels('advanced', left, right)
698                 self.SizeLabelWidths(left, right)
699
700                 #Plugin page
701                 self.pluginPanel = pluginPanel.pluginPanel(self.nb, callback)
702                 self.nb.AddPage(self.pluginPanel, _("Plugins"))
703
704                 #Alteration page
705                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
706                         self.alterationPanel = None
707                 else:
708                         self.alterationPanel = alterationPanel.alterationPanel(self.nb, callback)
709                         self.nb.AddPage(self.alterationPanel, _("Start/End-GCode"))
710
711                 self.Bind(wx.EVT_SIZE, self.OnSize)
712
713                 self.nb.SetSize(self.GetSize())
714                 self.UpdateSize(self.printPanel)
715                 self.UpdateSize(self.advancedPanel)
716
717         def _addSettingsToPanels(self, category, left, right):
718                 count = len(profile.getSubCategoriesFor(category)) + len(profile.getSettingsForCategory(category))
719
720                 p = left
721                 n = 0
722                 for title in profile.getSubCategoriesFor(category):
723                         n += 1 + len(profile.getSettingsForCategory(category, title))
724                         if n > count / 2:
725                                 p = right
726                         configBase.TitleRow(p, _(title))
727                         for s in profile.getSettingsForCategory(category, title):
728                                 configBase.SettingRow(p, s.getName())
729
730         def SizeLabelWidths(self, left, right):
731                 leftWidth = self.getLabelColumnWidth(left)
732                 rightWidth = self.getLabelColumnWidth(right)
733                 maxWidth = max(leftWidth, rightWidth)
734                 self.setLabelColumnWidth(left, maxWidth)
735                 self.setLabelColumnWidth(right, maxWidth)
736
737         def OnSize(self, e):
738                 # Make the size of the Notebook control the same size as this control
739                 self.nb.SetSize(self.GetSize())
740
741                 # Propegate the OnSize() event (just in case)
742                 e.Skip()
743
744                 # Perform out resize magic
745                 self.UpdateSize(self.printPanel)
746                 self.UpdateSize(self.advancedPanel)
747
748         def UpdateSize(self, configPanel):
749                 sizer = configPanel.GetSizer()
750
751                 # Pseudocde
752                 # if horizontal:
753                 #     if width(col1) < best_width(col1) || width(col2) < best_width(col2):
754                 #         switch to vertical
755                 # else:
756                 #     if width(col1) > (best_width(col1) + best_width(col1)):
757                 #         switch to horizontal
758                 #
759
760                 col1 = configPanel.leftPanel
761                 colSize1 = col1.GetSize()
762                 colBestSize1 = col1.GetBestSize()
763                 col2 = configPanel.rightPanel
764                 colSize2 = col2.GetSize()
765                 colBestSize2 = col2.GetBestSize()
766
767                 orientation = sizer.GetOrientation()
768
769                 if orientation == wx.HORIZONTAL:
770                         if (colSize1[0] <= colBestSize1[0]) or (colSize2[0] <= colBestSize2[0]):
771                                 configPanel.Freeze()
772                                 sizer = wx.BoxSizer(wx.VERTICAL)
773                                 sizer.Add(configPanel.leftPanel, flag=wx.EXPAND)
774                                 sizer.Add(configPanel.rightPanel, flag=wx.EXPAND)
775                                 configPanel.SetSizer(sizer)
776                                 #sizer.Layout()
777                                 configPanel.Layout()
778                                 self.Layout()
779                                 configPanel.Thaw()
780                 else:
781                         if max(colSize1[0], colSize2[0]) > (colBestSize1[0] + colBestSize2[0]):
782                                 configPanel.Freeze()
783                                 sizer = wx.BoxSizer(wx.HORIZONTAL)
784                                 sizer.Add(configPanel.leftPanel, proportion=1, border=35, flag=wx.EXPAND)
785                                 sizer.Add(configPanel.rightPanel, proportion=1, flag=wx.EXPAND)
786                                 configPanel.SetSizer(sizer)
787                                 #sizer.Layout()
788                                 configPanel.Layout()
789                                 self.Layout()
790                                 configPanel.Thaw()
791
792         def updateProfileToControls(self):
793                 super(normalSettingsPanel, self).updateProfileToControls()
794                 if self.alterationPanel is not None:
795                         self.alterationPanel.updateProfileToControls()
796                 self.pluginPanel.updateProfileToControls()