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