chiark / gitweb /
Merge tag '15.01-RC7' 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').startswith('ultimaker2') or \
377                    profile.getMachineSetting('machine_type').startswith('lulzbot_'):
378                         self.bedLevelWizardMenuItem.Enable(False)
379                         self.headOffsetWizardMenuItem.Enable(False)
380                 else:
381                         self.bedLevelWizardMenuItem.Enable(True)
382                         self.headOffsetWizardMenuItem.Enable(False)
383                 if int(profile.getMachineSetting('extruder_amount')) < 2:
384                         self.headOffsetWizardMenuItem.Enable(False)
385                 self.scene.updateProfileToControls()
386                 self.scene._scene.pushFree()
387
388         def onOneAtATimeSwitch(self, e):
389                 profile.putPreference('oneAtATime', self.oneAtATime.IsChecked())
390                 if self.oneAtATime.IsChecked() and profile.getMachineSettingFloat('extruder_head_size_height') < 1:
391                         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)
392                 self.scene.updateProfileToControls()
393                 self.scene._scene.pushFree()
394                 self.scene.sceneUpdated()
395
396         def OnPreferences(self, e):
397                 prefDialog = preferencesDialog.preferencesDialog(self)
398                 prefDialog.Centre()
399                 prefDialog.Show()
400                 prefDialog.Raise()
401
402         def OnMachineSettings(self, e):
403                 prefDialog = preferencesDialog.machineSettingsDialog(self)
404                 prefDialog.Centre()
405                 prefDialog.Show()
406                 prefDialog.Raise()
407
408         def OnDropFiles(self, files):
409                 self.scene.loadFiles(files)
410
411         def OnModelMRU(self, e):
412                 fileNum = e.GetId() - self.ID_MRU_MODEL1
413                 path = self.modelFileHistory.GetHistoryFile(fileNum)
414                 # Update Model MRU
415                 self.modelFileHistory.AddFileToHistory(path)  # move up the list
416                 self.config.SetPath("/ModelMRU")
417                 self.modelFileHistory.Save(self.config)
418                 self.config.Flush()
419                 # Load Model
420                 profile.putPreference('lastFile', path)
421                 filelist = [ path ]
422                 self.scene.loadFiles(filelist)
423
424         def addToModelMRU(self, file):
425                 self.modelFileHistory.AddFileToHistory(file)
426                 self.config.SetPath("/ModelMRU")
427                 self.modelFileHistory.Save(self.config)
428                 self.config.Flush()
429
430         def OnProfileMRU(self, e):
431                 fileNum = e.GetId() - self.ID_MRU_PROFILE1
432                 path = self.profileFileHistory.GetHistoryFile(fileNum)
433                 # Update Profile MRU
434                 self.profileFileHistory.AddFileToHistory(path)  # move up the list
435                 self.config.SetPath("/ProfileMRU")
436                 self.profileFileHistory.Save(self.config)
437                 self.config.Flush()
438                 # Load Profile
439                 profile.loadProfile(path)
440                 self.updateProfileToAllControls()
441
442         def addToProfileMRU(self, file):
443                 self.profileFileHistory.AddFileToHistory(file)
444                 self.config.SetPath("/ProfileMRU")
445                 self.profileFileHistory.Save(self.config)
446                 self.config.Flush()
447
448         def updateProfileToAllControls(self):
449                 self.scene.updateProfileToControls()
450                 self.normalSettingsPanel.updateProfileToControls()
451                 self.simpleSettingsPanel.updateProfileToControls()
452
453         def reloadSettingPanels(self, changedSliceMode = False):
454                 self.leftSizer.Detach(self.simpleSettingsPanel)
455                 self.leftSizer.Detach(self.normalSettingsPanel)
456                 self.simpleSettingsPanel.Destroy()
457                 self.normalSettingsPanel.Destroy()
458                 self.simpleSettingsPanel = simpleMode.simpleModePanel(self.leftPane, lambda : self.scene.sceneUpdated())
459                 self.normalSettingsPanel = normalSettingsPanel(self.leftPane, lambda : self.scene.sceneUpdated())
460                 self.leftSizer.Add(self.simpleSettingsPanel, 1)
461                 self.leftSizer.Add(self.normalSettingsPanel, 1, wx.EXPAND)
462                 self.updateSliceMode(changedSliceMode)
463                 self.updateProfileToAllControls()
464
465         def updateMachineMenu(self):
466                 #Remove all items so we can rebuild the menu. Inserting items seems to cause crashes, so this is the safest way.
467                 for item in self.machineMenu.GetMenuItems():
468                         self.machineMenu.RemoveItem(item)
469
470                 #Add a menu item for each machine configuration.
471                 for n in xrange(0, profile.getMachineCount()):
472                         i = self.machineMenu.Append(n + 0x1000, profile.getMachineSetting('machine_name', n).title(), kind=wx.ITEM_RADIO)
473                         if n == int(profile.getPreferenceFloat('active_machine')):
474                                 i.Check(True)
475                         self.Bind(wx.EVT_MENU, lambda e: self.OnSelectMachine(e.GetId() - 0x1000), i)
476
477                 self.machineMenu.AppendSeparator()
478                 i = self.machineMenu.Append(-1, _("Add new machine..."))
479                 self.Bind(wx.EVT_MENU, self.OnAddNewMachine, i)
480                 i = self.machineMenu.Append(-1, _("Machine settings..."))
481                 self.Bind(wx.EVT_MENU, self.OnMachineSettings, i)
482
483                 #Add tools for machines.
484                 self.machineMenu.AppendSeparator()
485
486                 self.defaultFirmwareInstallMenuItem = self.machineMenu.Append(-1, _("Install default firmware..."))
487                 self.Bind(wx.EVT_MENU, self.OnDefaultMarlinFirmware, self.defaultFirmwareInstallMenuItem)
488
489                 i = self.machineMenu.Append(-1, _("Install custom firmware..."))
490                 self.Bind(wx.EVT_MENU, self.OnCustomFirmware, i)
491
492         def OnLoadProfile(self, e):
493                 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)
494                 dlg.SetWildcard("ini files (*.ini)|*.ini")
495                 if dlg.ShowModal() == wx.ID_OK:
496                         profileFile = dlg.GetPath()
497                         profile.loadProfile(profileFile)
498                         self.updateProfileToAllControls()
499
500                         # Update the Profile MRU
501                         self.addToProfileMRU(profileFile)
502                 dlg.Destroy()
503
504         def OnLoadProfileFromGcode(self, e):
505                 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)
506                 dlg.SetWildcard("gcode files (*%s)|*%s;*%s" % (profile.getGCodeExtension(), profile.getGCodeExtension(), profile.getGCodeExtension()[0:2]))
507                 if dlg.ShowModal() == wx.ID_OK:
508                         gcodeFile = dlg.GetPath()
509                         f = open(gcodeFile, 'r')
510                         hasProfile = False
511                         for line in f:
512                                 if line.startswith(';CURA_PROFILE_STRING:'):
513                                         profile.setProfileFromString(line[line.find(':')+1:].strip())
514                                         if ';{profile_string}' not in profile.getAlterationFile('end.gcode'):
515                                                 profile.setAlterationFile('end.gcode', profile.getAlterationFile('end.gcode') + '\n;{profile_string}')
516                                         hasProfile = True
517                         if hasProfile:
518                                 self.updateProfileToAllControls()
519                         else:
520                                 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)
521                 dlg.Destroy()
522
523         def OnSaveProfile(self, e):
524                 dlg=wx.FileDialog(self, _("Select profile file to save"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
525                 dlg.SetWildcard("ini files (*.ini)|*.ini")
526                 if dlg.ShowModal() == wx.ID_OK:
527                         profile_filename = dlg.GetPath()
528                         if not profile_filename.lower().endswith('.ini'): #hack for linux, as for some reason the .ini is not appended.
529                                 profile_filename += '.ini'
530                         profile.saveProfile(profile_filename)
531                 dlg.Destroy()
532
533         def OnSaveDifferences(self, e):
534                 dlg=wx.FileDialog(self, _("Select profile file to save"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_SAVE)
535                 dlg.SetWildcard("ini files (*.ini)|*.ini")
536                 if dlg.ShowModal() == wx.ID_OK:
537                         profile_filename = dlg.GetPath()
538                         if not profile_filename.lower().endswith('.ini'): #hack for linux, as for some reason the .ini is not appended.
539                                 profile_filename += '.ini'
540                         profile.saveProfileDifferenceFromDefault(profile_filename)
541                 dlg.Destroy()
542
543         def OnResetProfile(self, e):
544                 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)
545                 result = dlg.ShowModal() == wx.ID_YES
546                 dlg.Destroy()
547                 if result:
548                         profile.resetProfile()
549                         self.updateProfileToAllControls()
550
551         def OnSimpleSwitch(self, e):
552                 profile.putPreference('startMode', 'Simple')
553                 self.updateSliceMode()
554
555         def OnNormalSwitch(self, e):
556                 profile.putPreference('startMode', 'Normal')
557                 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)
558                 result = dlg.ShowModal() == wx.ID_YES
559                 dlg.Destroy()
560                 if result:
561                         profile.resetProfile()
562                         for k, v in self.simpleSettingsPanel.getSettingOverrides().items():
563                                 profile.putProfileSetting(k, v)
564                         self.updateProfileToAllControls()
565                 self.updateSliceMode()
566
567         def OnDefaultMarlinFirmware(self, e):
568                 firmwareInstall.InstallFirmware(self)
569
570         def OnCustomFirmware(self, e):
571                 if profile.getMachineSetting('machine_type').startswith('ultimaker'):
572                         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)
573                 dlg=wx.FileDialog(self, _("Open firmware to upload"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
574                 dlg.SetWildcard("HEX file (*.hex)|*.hex;*.HEX")
575                 if dlg.ShowModal() == wx.ID_OK:
576                         filename = dlg.GetPath()
577                         dlg.Destroy()
578                         if not(os.path.exists(filename)):
579                                 return
580                         #For some reason my Ubuntu 10.10 crashes here.
581                         firmwareInstall.InstallFirmware(self, filename)
582
583         def OnAddNewMachine(self, e):
584                 self.Hide()
585                 wasSimple = profile.getPreference('startMode') == 'Simple'
586                 configWizard.ConfigWizard(True)
587                 isSimple = profile.getPreference('startMode') == 'Simple'
588                 self.Show()
589                 self.reloadSettingPanels(isSimple != wasSimple)
590                 self.updateMachineMenu()
591
592         def OnSelectMachine(self, index):
593                 profile.setActiveMachine(index)
594                 self.reloadSettingPanels(False)
595
596         def OnBedLevelWizard(self, e):
597                 configWizard.bedLevelWizard()
598
599         def OnHeadOffsetWizard(self, e):
600                 configWizard.headOffsetWizard()
601
602         def OnExpertOpen(self, e):
603                 ecw = expertConfig.expertConfigWindow(lambda : self.scene.sceneUpdated())
604                 ecw.Centre()
605                 ecw.Show()
606
607         def OnMinecraftImport(self, e):
608                 mi = minecraftImport.minecraftImportWindow(self)
609                 mi.Centre()
610                 mi.Show(True)
611
612         def OnPIDDebugger(self, e):
613                 debugger = pidDebugger.debuggerWindow(self)
614                 debugger.Centre()
615                 debugger.Show(True)
616
617         def OnAutoFirmwareUpdate(self, e):
618                 dlg=wx.FileDialog(self, _("Open firmware to upload"), os.path.split(profile.getPreference('lastFile'))[0], style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
619                 dlg.SetWildcard("HEX file (*.hex)|*.hex;*.HEX")
620                 if dlg.ShowModal() == wx.ID_OK:
621                         filename = dlg.GetPath()
622                         dlg.Destroy()
623                         if not(os.path.exists(filename)):
624                                 return
625                         #For some reason my Ubuntu 10.10 crashes here.
626                         installer = firmwareInstall.AutoUpdateFirmware(self, filename)
627
628         def onCopyProfileClipboard(self, e):
629                 try:
630                         if not wx.TheClipboard.IsOpened():
631                                 wx.TheClipboard.Open()
632                                 clipData = wx.TextDataObject()
633                                 self.lastTriedClipboard = profile.getProfileString()
634                                 profileString = profile.insertNewlines("CURA_PROFILE_STRING:" + self.lastTriedClipboard)
635                                 clipData.SetText(profileString)
636                                 wx.TheClipboard.SetData(clipData)
637                                 wx.TheClipboard.Close()
638                 except:
639                         print "Could not write to clipboard, unable to get ownership. Another program is using the clipboard."
640
641         def OnCheckForUpdate(self, e):
642                 newVersion = version.checkForNewerVersion()
643                 if newVersion is not None:
644                         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:
645                                 webbrowser.open(newVersion)
646                 else:
647                         wx.MessageBox(_("You are running the latest version of Cura!"), _("Awesome!"), wx.ICON_INFORMATION)
648
649         def OnAbout(self, e):
650                 aboutBox = aboutWindow.aboutWindow(self)
651                 aboutBox.Centre()
652                 aboutBox.Show()
653                 aboutBox.Raise()
654
655         def OnClose(self, e):
656                 profile.saveProfile(profile.getDefaultProfilePath(), True)
657
658                 # Save the window position, size & state from the preferences file
659                 profile.putPreference('window_maximized', self.IsMaximized())
660                 if not self.IsMaximized() and not self.IsIconized():
661                         (posx, posy) = self.GetPosition()
662                         profile.putPreference('window_pos_x', posx)
663                         profile.putPreference('window_pos_y', posy)
664                         (width, height) = self.GetSize()
665                         profile.putPreference('window_width', width)
666                         profile.putPreference('window_height', height)
667
668                         # Save normal sash position.  If in normal mode (!simple mode), get last position of sash before saving it...
669                         isSimple = profile.getPreference('startMode') == 'Simple'
670                         if not isSimple:
671                                 self.normalSashPos = self.splitter.GetSashPosition()
672                         profile.putPreference('window_normal_sash', self.normalSashPos)
673
674                 #HACK: Set the paint function of the glCanvas to nothing so it won't keep refreshing. Which can keep wxWidgets from quiting.
675                 print "Closing down"
676                 self.scene.OnPaint = lambda e : e
677                 self.scene.cleanup()
678                 self.Destroy()
679
680         def OnQuit(self, e):
681                 self.Close()
682
683 class normalSettingsPanel(configBase.configPanelBase):
684         "Main user interface window"
685         def __init__(self, parent, callback = None):
686                 super(normalSettingsPanel, self).__init__(parent, callback)
687
688                 #Main tabs
689                 self.nb = wx.Notebook(self)
690                 self.SetSizer(wx.BoxSizer(wx.HORIZONTAL))
691                 self.GetSizer().Add(self.nb, 1, wx.EXPAND)
692
693                 (left, right, self.printPanel) = self.CreateDynamicConfigTab(self.nb, _('Basic'))
694                 self._addSettingsToPanels('basic', left, right)
695                 self.SizeLabelWidths(left, right)
696
697                 (left, right, self.advancedPanel) = self.CreateDynamicConfigTab(self.nb, _('Advanced'))
698                 self._addSettingsToPanels('advanced', left, right)
699                 self.SizeLabelWidths(left, right)
700
701                 #Plugin page
702                 self.pluginPanel = pluginPanel.pluginPanel(self.nb, callback)
703                 self.nb.AddPage(self.pluginPanel, _("Plugins"))
704
705                 #Alteration page
706                 if profile.getMachineSetting('gcode_flavor') == 'UltiGCode':
707                         self.alterationPanel = None
708                 else:
709                         self.alterationPanel = alterationPanel.alterationPanel(self.nb, callback)
710                         self.nb.AddPage(self.alterationPanel, _("Start/End-GCode"))
711
712                 self.Bind(wx.EVT_SIZE, self.OnSize)
713
714                 self.nb.SetSize(self.GetSize())
715                 self.UpdateSize(self.printPanel)
716                 self.UpdateSize(self.advancedPanel)
717
718         def _addSettingsToPanels(self, category, left, right):
719                 count = len(profile.getSubCategoriesFor(category)) + len(profile.getSettingsForCategory(category))
720
721                 p = left
722                 n = 0
723                 for title in profile.getSubCategoriesFor(category):
724                         n += 1 + len(profile.getSettingsForCategory(category, title))
725                         if n > count / 2:
726                                 p = right
727                         configBase.TitleRow(p, _(title))
728                         for s in profile.getSettingsForCategory(category, title):
729                                 configBase.SettingRow(p, s.getName())
730
731         def SizeLabelWidths(self, left, right):
732                 leftWidth = self.getLabelColumnWidth(left)
733                 rightWidth = self.getLabelColumnWidth(right)
734                 maxWidth = max(leftWidth, rightWidth)
735                 self.setLabelColumnWidth(left, maxWidth)
736                 self.setLabelColumnWidth(right, maxWidth)
737
738         def OnSize(self, e):
739                 # Make the size of the Notebook control the same size as this control
740                 self.nb.SetSize(self.GetSize())
741
742                 # Propegate the OnSize() event (just in case)
743                 e.Skip()
744
745                 # Perform out resize magic
746                 self.UpdateSize(self.printPanel)
747                 self.UpdateSize(self.advancedPanel)
748
749         def UpdateSize(self, configPanel):
750                 sizer = configPanel.GetSizer()
751
752                 # Pseudocde
753                 # if horizontal:
754                 #     if width(col1) < best_width(col1) || width(col2) < best_width(col2):
755                 #         switch to vertical
756                 # else:
757                 #     if width(col1) > (best_width(col1) + best_width(col1)):
758                 #         switch to horizontal
759                 #
760
761                 col1 = configPanel.leftPanel
762                 colSize1 = col1.GetSize()
763                 colBestSize1 = col1.GetBestSize()
764                 col2 = configPanel.rightPanel
765                 colSize2 = col2.GetSize()
766                 colBestSize2 = col2.GetBestSize()
767
768                 orientation = sizer.GetOrientation()
769
770                 if orientation == wx.HORIZONTAL:
771                         if (colSize1[0] <= colBestSize1[0]) or (colSize2[0] <= colBestSize2[0]):
772                                 configPanel.Freeze()
773                                 sizer = wx.BoxSizer(wx.VERTICAL)
774                                 sizer.Add(configPanel.leftPanel, flag=wx.EXPAND)
775                                 sizer.Add(configPanel.rightPanel, flag=wx.EXPAND)
776                                 configPanel.SetSizer(sizer)
777                                 #sizer.Layout()
778                                 configPanel.Layout()
779                                 self.Layout()
780                                 configPanel.Thaw()
781                 else:
782                         if max(colSize1[0], colSize2[0]) > (colBestSize1[0] + colBestSize2[0]):
783                                 configPanel.Freeze()
784                                 sizer = wx.BoxSizer(wx.HORIZONTAL)
785                                 sizer.Add(configPanel.leftPanel, proportion=1, border=35, flag=wx.EXPAND)
786                                 sizer.Add(configPanel.rightPanel, proportion=1, flag=wx.EXPAND)
787                                 configPanel.SetSizer(sizer)
788                                 #sizer.Layout()
789                                 configPanel.Layout()
790                                 self.Layout()
791                                 configPanel.Thaw()
792
793         def updateProfileToControls(self):
794                 super(normalSettingsPanel, self).updateProfileToControls()
795                 if self.alterationPanel is not None:
796                         self.alterationPanel.updateProfileToControls()
797                 self.pluginPanel.updateProfileToControls()