Ejemplo n.º 1
0
class App(BaseApp):

    TATLIN_VERSION = '0.2.5'
    TATLIN_LICENSE = """This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""
    RECENT_FILE_LIMIT = 10

    def __init__(self):
        super(App, self).__init__()

        # ---------------------------------------------------------------------
        # WINDOW SETUP
        # ---------------------------------------------------------------------
        self.window = MainWindow()

        self.icon = load_icon(resolve_path('tatlin-logo.png'))
        self.window.set_icon(self.icon)

        # ---------------------------------------------------------------------
        # APP SETUP
        # ---------------------------------------------------------------------

        self.init_config()

        recent_files = self.config.read('ui.recent_files')
        if recent_files:
            self.recent_files = []
            for f in recent_files.split(os.path.pathsep):
                if f[-1] in ['0', '1', '2']:
                    fpath, ftype = f[:-1], OpenDialog.ftypes[int(f[-1])]
                else:
                    fpath, ftype = f, None

                if os.path.exists(fpath):
                    self.recent_files.append(
                        (os.path.basename(fpath), fpath, ftype))
            self.recent_files = self.recent_files[:self.RECENT_FILE_LIMIT]

        else:
            self.recent_files = []
        self.window.update_recent_files_menu(self.recent_files)

        window_w = self.config.read('ui.window_w', int)
        window_h = self.config.read('ui.window_h', int)
        self.window.set_size((window_w, window_h))

        self.init_scene()

    def init_config(self):
        fname = os.path.expanduser(os.path.join('~', '.tatlin'))
        self.config = Config(fname)

    def init_scene(self):
        self.panel = None
        self.scene = None
        self.model_file = None

        # dict of properties that other components can read from the app
        self._app_properties = {
            'layers_range_max': lambda: self.scene.get_property('max_layers'),
            'layers_value': lambda: self.scene.get_property('max_layers'),
            'scaling-factor': self.model_scaling_factor,
            'width': self.model_width,
            'depth': self.model_depth,
            'height': self.model_height,
            'rotation-x': self.model_rotation_x,
            'rotation-y': self.model_rotation_y,
            'rotation-z': self.model_rotation_z,
        }

    def show_window(self):
        self.window.show_all()

    # -------------------------------------------------------------------------
    # PROPERTIES
    # -------------------------------------------------------------------------

    def get_property(self, name):
        """
        Return a property of the application.
        """
        return self._app_properties[name]()

    def model_scaling_factor(self):
        factor = self.scene.get_property('scaling-factor')
        return format_float(factor)

    def model_width(self):
        width = self.scene.get_property('width')
        return format_float(width)

    def model_depth(self):
        depth = self.scene.get_property('depth')
        return format_float(depth)

    def model_height(self):
        height = self.scene.get_property('height')
        return format_float(height)

    def model_rotation_x(self):
        angle = self.scene.get_property('rotation-x')
        return format_float(angle)

    def model_rotation_y(self):
        angle = self.scene.get_property('rotation-y')
        return format_float(angle)

    def model_rotation_z(self):
        angle = self.scene.get_property('rotation-z')
        return format_float(angle)

    @property
    def current_dir(self):
        """
        Return path where a file should be saved to.
        """
        if self.model_file is not None:
            dur = self.model_file.dirname
        elif len(self.recent_files) > 0:
            dur = os.path.dirname(self.recent_files[0][1])
        else:
            dur = os.getcwd()

        return dur

    def command_line(self):
        if len(sys.argv) > 1:
            self.open_and_display_file(os.path.abspath(sys.argv[1]))

    # -------------------------------------------------------------------------
    # EVENT HANDLERS
    # -------------------------------------------------------------------------

    def on_file_open(self, event=None):
        if self.save_changes_dialog():
            show_again = True

            while show_again:
                dialog = OpenDialog(self.window, self.current_dir)
                fpath = dialog.get_path()
                if fpath:
                    show_again = not self.open_and_display_file(
                        fpath, dialog.get_type())
                else:
                    show_again = False

    def on_file_save(self, event=None):
        """
        Save changes to the same file.
        """
        self.scene.export_to_file(self.model_file)
        self.window.file_modified = False

    def on_file_save_as(self, event=None):
        """
        Save changes to a new file.
        """
        dialog = SaveDialog(self.window, self.current_dir)
        fpath = dialog.get_path()
        if fpath:
            stl_file = ModelFile(fpath)
            self.scene.export_to_file(stl_file)
            self.model_file = stl_file
            self.window.filename = stl_file.basename
            self.window.file_modified = False

    def on_quit(self, event=None):
        """
        On quit, write config settings and show a dialog proposing to save the
        changes if the scene has been modified.
        """
        try:
            self.config.write(
                'ui.recent_files',
                os.path.pathsep.join([
                    f[1] + str(OpenDialog.ftypes.index(f[2]))
                    for f in self.recent_files
                ]))

            w, h = self.window.get_size()
            self.config.write('ui.window_w', w)
            self.config.write('ui.window_h', h)

            if self.scene:
                self.config.write('ui.gcode_2d', int(self.scene.mode_2d))

            self.config.commit()
        except IOError:
            logging.warning('Could not write settings to config file %s' %
                            self.config.fname)

        if self.save_changes_dialog():
            self.window.quit()

    def save_changes_dialog(self):
        proceed = True
        if self.scene and self.scene.model_modified:
            ask_again = True

            while ask_again:
                dialog = QuitDialog(self.window)
                response = dialog.show()
                if response == QuitDialog.RESPONSE_SAVE:
                    self.on_file_save()
                    ask_again = False
                elif response == QuitDialog.RESPONSE_SAVE_AS:
                    self.on_file_save_as()
                    ask_again = self.scene.model_modified
                elif response == QuitDialog.RESPONSE_CANCEL:
                    ask_again = False
                    proceed = False
                elif response == QuitDialog.RESPONSE_DISCARD:
                    ask_again = False
                else:
                    logging.warning('Unknown dialog response: %s' % response)
                    ask_again = False
                    proceed = False

        return proceed

    def on_about(self, event=None):
        AboutDialog()

    def scaling_factor_changed(self, factor):
        try:
            self.scene.scale_model(float(factor))
            self.scene.invalidate()
            # tell all the widgets that care about model size that it has changed
            self.panel.model_size_changed()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass  # ignore invalid values

    def dimension_changed(self, dimension, value):
        try:
            self.scene.change_model_dimension(dimension, float(value))
            self.scene.invalidate()
            self.panel.model_size_changed()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass  # ignore invalid values

    def on_layers_changed(self, layers):
        self.scene.change_num_layers(layers)
        self.scene.invalidate()

    def rotation_changed(self, axis, angle):
        try:
            self.scene.rotate_model(float(angle), axis)
            self.scene.invalidate()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass  # ignore invalid values

    def on_center_model(self):
        """
        Center model on platform.
        """
        self.scene.center_model()
        self.scene.invalidate()
        self.window.file_modified = self.scene.model_modified

    def on_arrows_toggled(self, value):
        """
        Show/hide arrows on the Gcode model.
        """
        self.scene.show_arrows(value)
        self.scene.invalidate()

    def on_reset_view(self):
        """
        Restore the view of the model shown on startup.
        """
        self.scene.reset_view()
        self.scene.invalidate()

    def on_set_mode(self, value):
        self.scene.mode_2d = not value
        if self.scene.initialized:
            self.scene.invalidate()

    def on_set_ortho(self, value):
        self.scene.mode_ortho = value
        self.scene.invalidate()

    def on_view_front(self):
        self.scene.rotate_view(0, 0)

    def on_view_back(self):
        self.scene.rotate_view(180, 0)

    def on_view_left(self):
        self.scene.rotate_view(90, 0)

    def on_view_right(self):
        self.scene.rotate_view(-90, 0)

    def on_view_top(self):
        self.scene.rotate_view(0, -90)

    def on_view_bottom(self):
        self.scene.rotate_view(0, 90)

    # -------------------------------------------------------------------------
    # FILE OPERATIONS
    # -------------------------------------------------------------------------

    def update_recent_files(self, fpath, ftype=None):
        self.recent_files = [f for f in self.recent_files if f[1] != fpath]
        self.recent_files.insert(0, (os.path.basename(fpath), fpath, ftype))
        self.recent_files = self.recent_files[:self.RECENT_FILE_LIMIT]
        self.window.update_recent_files_menu(self.recent_files)

    def open_and_display_file(self, fpath, ftype=None):
        self.set_wait_cursor()
        progress_dialog_read = None
        progress_dialog_load = None
        success = True

        try:
            self.update_recent_files(fpath, ftype)
            self.model_file = ModelFile(fpath, ftype)

            self.scene = Scene(self.window)

            progress_dialog_read = ProgressDialog('Reading file...')
            model, model_data = self.model_file.read(progress_dialog_read.step)

            progress_dialog_load = ProgressDialog('Loading model...')
            model.load_data(model_data, progress_dialog_load.step)

            self.scene.clear()
            self.scene.add_model(model)

            if self.model_file.filetype == 'gcode':
                offset_x = self.config.read('machine.platform_offset_x', float)
                offset_y = self.config.read('machine.platform_offset_y', float)
                offset_z = self.config.read('machine.platform_offset_z', float)

                if offset_x is None and offset_y is None and offset_z is None:
                    self.scene.view_model_center()
                    logging.info(
                        'Platform offsets not set, showing model in the center'
                    )
                else:
                    model.offset_x = offset_x if offset_x is not None else 0
                    model.offset_y = offset_y if offset_y is not None else 0
                    model.offset_z = offset_z if offset_z is not None else 0
                    logging.info(
                        'Using platform offsets: (%s, %s, %s)' %
                        (model.offset_x, model.offset_y, model.offset_z))

            # platform needs to be added last to be translucent
            platform_w = self.config.read('machine.platform_w', float)
            platform_d = self.config.read('machine.platform_d', float)
            platform = Platform(platform_w, platform_d)
            self.scene.add_supporting_actor(platform)

            self.panel = self.create_panel()
            # update panel to reflect new model properties
            self.panel.set_initial_values()
            self.panel.connect_handlers()

            # always start with the same view on the scene
            self.scene.reset_view(True)
            if self.model_file.filetype == 'gcode':
                self.scene.mode_2d = bool(self.config.read('ui.gcode_2d', int))
            else:
                self.scene.mode_2d = False

            if hasattr(self.panel, 'set_3d_view'):
                self.panel.set_3d_view(not self.scene.mode_2d)

            self.window.set_file_widgets(self.scene, self.panel)
            self.window.filename = self.model_file.basename
            self.window.file_modified = False
            self.window.menu_enable_file_items(
                self.model_file.filetype != 'gcode')

            if self.model_file.size > 2**30:
                size = self.model_file.size / 2**30
                units = 'GB'
            elif self.model_file.size > 2**20:
                size = self.model_file.size / 2**20
                units = 'MB'
            elif self.model_file.size > 2**10:
                size = self.model_file.size / 2**10
                units = 'KB'
            else:
                size = self.model_file.size
                units = 'B'

            vertex_plural = 'vertex' if int(str(
                model.vertex_count)[-1]) == 1 else 'vertices'
            self.window.update_status(' %s (%.1f%s, %d %s)' %
                                      (self.model_file.basename, size, units,
                                       model.vertex_count, vertex_plural))
        except IOError, e:
            self.set_normal_cursor()
            error_dialog = OpenErrorAlert(fpath, e.strerror)
            error_dialog.show()
            success = False
        except ModelFileError, e:
            self.set_normal_cursor()
            error_dialog = OpenErrorAlert(fpath, e.message)
            error_dialog.show()
            success = False
Ejemplo n.º 2
0
 def init_config(self):
     fname = os.path.expanduser(os.path.join('~', '.tatlin'))
     self.config = Config(fname)
Ejemplo n.º 3
0
class App(object):

    def __init__(self):
        # ---------------------------------------------------------------------
        # WINDOW SETUP
        # ---------------------------------------------------------------------

        self.window = MainWindow()

        self.actiongroup = self.set_up_actions()
        self.create_menu_items(self.actiongroup)
        for menu_item in self.menu_items:
            self.window.append_menu_item(menu_item)

        self.menu_enable_file_items(False)

        self.window.connect('destroy',      self.quit)
        self.window.connect('open-clicked', self.open_file_dialog)

        # ---------------------------------------------------------------------
        # APP SETUP
        # ---------------------------------------------------------------------

        self.init_config()
        self.init_scene()

    def init_config(self):
        fname = os.path.expanduser(os.path.join('~', '.tatlin'))
        self.config = Config(fname)

    def init_scene(self):
        self.panel = None
        self.scene = None
        self.model_file = None

        # dict of properties that other components can read from the app
        self._app_properties = {
            'layers_range_max': lambda: self.scene.get_property('max_layers'),
            'layers_value':     lambda: self.scene.get_property('max_layers'),
            'scaling-factor':   self.model_scaling_factor,
            'width':            self.model_width,
            'depth':            self.model_depth,
            'height':           self.model_height,
            'rotation-x':       self.model_rotation_x,
            'rotation-y':       self.model_rotation_y,
            'rotation-z':       self.model_rotation_z,
        }

    def show_window(self):
        self.window.show_all()

    # -------------------------------------------------------------------------
    # ACTIONS
    # -------------------------------------------------------------------------

    def set_up_actions(self):
        actiongroup = ActionGroup('main')
        actiongroup.add_action(gtk.Action('file', '_File', 'File', None))

        action_open = gtk.Action('open', 'Open', 'Open', gtk.STOCK_OPEN)
        action_open.connect('activate', self.open_file_dialog)
        actiongroup.add_action_with_accel(action_open, '<Control>o')

        action_save = gtk.Action('save', 'Save', 'Save file', gtk.STOCK_SAVE)
        action_save.connect('activate', self.save_file)
        actiongroup.add_action_with_accel(action_save, '<Control>s')

        save_as = gtk.Action('save-as', 'Save As...', 'Save As...', gtk.STOCK_SAVE_AS)
        save_as.connect('activate', self.save_file_as)
        actiongroup.add_action_with_accel(save_as, '<Control><Shift>s')

        action_quit = gtk.Action('quit', 'Quit', 'Quit', gtk.STOCK_QUIT)
        action_quit.connect('activate', self.quit)
        actiongroup.add_action_with_accel(action_quit, '<Control>q')

        accelgroup = gtk.AccelGroup()
        for action in actiongroup.list_actions():
            action.set_accel_group(accelgroup)

        self.window.add_accel_group(accelgroup)

        return actiongroup

    def create_menu_items(self, actiongroup):
        file_menu = gtk.Menu()
        file_menu.append(actiongroup.menu_item('open'))

        save_item = actiongroup.menu_item('save')
        file_menu.append(save_item)

        save_as_item = actiongroup.menu_item('save-as')
        file_menu.append(save_as_item)

        file_menu.append(actiongroup.menu_item('quit'))

        item_file = actiongroup.menu_item('file')
        item_file.set_submenu(file_menu)

        self.menu_items_file = [save_item, save_as_item]
        self.menu_items = [item_file]

    def menu_enable_file_items(self, enable=True):
        for menu_item in self.menu_items_file:
            menu_item.set_sensitive(enable)

    # -------------------------------------------------------------------------
    # PROPERTIES
    # -------------------------------------------------------------------------

    def get_property(self, name):
        """
        Return a property of the application.
        """
        return self._app_properties[name]()

    def model_scaling_factor(self):
        factor = self.scene.get_property('scaling-factor')
        return format_float(factor)

    def model_width(self):
        width = self.scene.get_property('width')
        return format_float(width)

    def model_depth(self):
        depth = self.scene.get_property('depth')
        return format_float(depth)

    def model_height(self):
        height = self.scene.get_property('height')
        return format_float(height)

    def model_rotation_x(self):
        angle = self.scene.get_property('rotation-x')
        return format_float(angle)

    def model_rotation_y(self):
        angle = self.scene.get_property('rotation-y')
        return format_float(angle)

    def model_rotation_z(self):
        angle = self.scene.get_property('rotation-z')
        return format_float(angle)

    @property
    def current_dir(self):
        """
        Return path where a file should be saved to.
        """
        if self.model_file is not None:
            dur = self.model_file.dirname
        else:
            dur = os.getcwd()
        return dur

    # -------------------------------------------------------------------------
    # EVENT HANDLERS
    # -------------------------------------------------------------------------

    def command_line(self):
        if len(sys.argv) > 1:
            self.open_and_display_file(sys.argv[1])

    def save_file(self, action=None):
        """
        Save changes to the same file.
        """
        self.scene.export_to_file(self.model_file)
        self.window.file_modified = False

    def save_file_as(self, action=None):
        """
        Save changes to a new file.
        """
        dialog = SaveDialog(self.current_dir)

        if dialog.run() == gtk.RESPONSE_ACCEPT:
            stl_file = ModelFile(dialog.get_filename())
            self.scene.export_to_file(stl_file)
            self.model_file = stl_file
            self.window.filename = stl_file.basename
            self.window.file_modified = False

        dialog.destroy()

    def open_file_dialog(self, action=None):
        if self.save_changes_dialog():
            dialog = OpenDialog(self.current_dir)
            show_again = True

            while show_again:
                if dialog.run() == gtk.RESPONSE_ACCEPT:
                    dialog.hide()
                    fname = dialog.get_filename()
                    show_again = not self.open_and_display_file(fname)
                else:
                    show_again = False

            dialog.destroy()

    def quit(self, action=None):
        """
        On quit, show a dialog proposing to save the changes if the scene has
        been modified.
        """
        if self.save_changes_dialog():
            gtk.main_quit()

    def save_changes_dialog(self):
        proceed = True
        if self.scene and self.scene.model_modified:
            dialog = QuitDialog(self.window)
            ask_again = True

            while ask_again:
                response = dialog.run()
                if response == QuitDialog.RESPONSE_SAVE:
                    self.save_file()
                    ask_again = False
                elif response == QuitDialog.RESPONSE_SAVE_AS:
                    self.save_file_as()
                    ask_again = self.scene.model_modified
                elif response in [QuitDialog.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
                    ask_again = False
                    proceed = False
                elif response == QuitDialog.RESPONSE_DISCARD:
                    ask_again = False

            dialog.destroy()

        return proceed

    def scaling_factor_changed(self, factor):
        try:
            factor = float(factor)
            self.scene.scale_model(factor)
            self.scene.invalidate()
            # tell all the widgets that care about model size that it has changed
            self.panel.model_size_changed()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass # ignore invalid values

    def dimension_changed(self, dimension, value):
        try:
            value = float(value)
            self.scene.change_model_dimension(dimension, value)
            self.scene.invalidate()
            self.panel.model_size_changed()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass # ignore invalid values

    def on_scale_value_changed(self, widget):
        value = int(widget.get_value())
        self.scene.change_num_layers(value)
        self.scene.invalidate()

    def rotation_changed(self, axis, angle):
        try:
            self.scene.rotate_model(float(angle), axis)
            self.scene.invalidate()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass # ignore invalid values

    def on_button_center_clicked(self, widget):
        """
        Center model on platform.
        """
        self.scene.center_model()
        self.scene.invalidate()
        self.window.file_modified = self.scene.model_modified

    def on_arrows_toggled(self, widget):
        """
        Show/hide arrows on the Gcode model.
        """
        self.scene.show_arrows(widget.get_active())
        self.scene.invalidate()

    def on_reset_view(self, widget):
        """
        Restore the view of the model shown on startup.
        """
        self.scene.reset_view()
        self.scene.invalidate()

    def on_set_mode(self, widget):
        self.scene.mode_2d = not widget.get_active()
        self.scene.invalidate()

    def on_set_ortho(self, widget):
        self.scene.mode_ortho = widget.get_active()
        self.scene.invalidate()

    def on_view_front(self, widget):
        self.scene.rotate_view(0, 0)

    def on_view_back(self, widget):
        self.scene.rotate_view(180, 0)

    def on_view_left(self, widget):
        self.scene.rotate_view(90, 0)

    def on_view_right(self, widget):
        self.scene.rotate_view(-90, 0)

    def on_view_top(self, widget):
        self.scene.rotate_view(0, -90)

    def on_view_bottom(self, widget):
        self.scene.rotate_view(0, 90)

    # -------------------------------------------------------------------------
    # FILE OPERATIONS
    # -------------------------------------------------------------------------

    def open_and_display_file(self, fpath):
        progress_dialog = ProgressDialog('Loading', self.window)
        self.window.set_cursor(gtk.gdk.WATCH)
        success = True

        try:
            self.model_file = ModelFile(fpath)

            if self.scene is None:
                self.scene = Scene()
                self.glarea = SceneArea(self.scene)

            progress_dialog.set_text('Reading file...')
            progress_dialog.show()
            model, model_data = self.model_file.read(progress_dialog.step)

            progress_dialog.set_text('Loading model...')
            model.load_data(model_data, progress_dialog.step)

            self.scene.clear()
            self.scene.add_model(model)

            # platform needs to be added last to be translucent
            platform_w = self.config.read('machine.platform_w', int)
            platform_d = self.config.read('machine.platform_d', int)
            platform = Platform(platform_w, platform_d)
            self.scene.add_supporting_actor(platform)

            if self.panel is None or not self.panel_matches_file():
                self.panel = self.create_panel()

            # update panel to reflect new model properties
            self.panel.set_initial_values()
            self.panel.connect_handlers()
            # always start with the same view on the scene
            self.scene.reset_view(True)
            self.scene.mode_2d = False

            self.window.set_file_widgets(self.glarea, self.panel)
            self.window.filename = self.model_file.basename
            self.menu_enable_file_items(self.model_file.filetype != 'gcode')
        except IOError, e:
            self.window.window.set_cursor(None)
            progress_dialog.hide()
            error_dialog = OpenErrorAlert(self.window, fpath, e.strerror)
            error_dialog.run()
            error_dialog.destroy()
            success = False
        except ModelFileError, e:
            self.window.window.set_cursor(None)
            progress_dialog.hide()
            error_dialog = OpenErrorAlert(self.window, fpath, e.message)
            error_dialog.run()
            error_dialog.destroy()
            success = False
Ejemplo n.º 4
0
 def init_config(self):
     fname = os.path.expanduser(os.path.join('~', '.tatlin'))
     self.config = Config(fname)
Ejemplo n.º 5
0
class App(BaseApp):

    TATLIN_VERSION = '0.2.5'
    TATLIN_LICENSE = """This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""
    RECENT_FILE_LIMIT = 10

    def __init__(self):
        super(App, self).__init__()

        # ---------------------------------------------------------------------
        # WINDOW SETUP
        # ---------------------------------------------------------------------
        self.window = MainWindow()

        self.icon = load_icon(resolve_path('tatlin-logo.png'))
        self.window.set_icon(self.icon)

        # ---------------------------------------------------------------------
        # APP SETUP
        # ---------------------------------------------------------------------

        self.init_config()

        recent_files = self.config.read('ui.recent_files')
        if recent_files:
            self.recent_files = []
            for f in recent_files.split(os.path.pathsep):
                if f[-1] in ['0', '1', '2']:
                    fpath, ftype = f[:-1], OpenDialog.ftypes[int(f[-1])]
                else:
                    fpath, ftype = f, None

                if os.path.exists(fpath):
                    self.recent_files.append((os.path.basename(fpath), fpath, ftype))
            self.recent_files = self.recent_files[:self.RECENT_FILE_LIMIT]

        else:
            self.recent_files = []
        self.window.update_recent_files_menu(self.recent_files)

        window_w = self.config.read('ui.window_w', int)
        window_h = self.config.read('ui.window_h', int)
        self.window.set_size((window_w, window_h))

        self.init_scene()

    def init_config(self):
        fname = os.path.expanduser(os.path.join('~', '.tatlin'))
        self.config = Config(fname)

    def init_scene(self):
        self.panel = None
        self.scene = None
        self.model_file = None

        # dict of properties that other components can read from the app
        self._app_properties = {
            'layers_range_max': lambda: self.scene.get_property('max_layers'),
            'layers_value':     lambda: self.scene.get_property('max_layers'),
            'scaling-factor':   self.model_scaling_factor,
            'width':            self.model_width,
            'depth':            self.model_depth,
            'height':           self.model_height,
            'rotation-x':       self.model_rotation_x,
            'rotation-y':       self.model_rotation_y,
            'rotation-z':       self.model_rotation_z,
        }

    def show_window(self):
        self.window.show_all()

    # -------------------------------------------------------------------------
    # PROPERTIES
    # -------------------------------------------------------------------------

    def get_property(self, name):
        """
        Return a property of the application.
        """
        return self._app_properties[name]()

    def model_scaling_factor(self):
        factor = self.scene.get_property('scaling-factor')
        return format_float(factor)

    def model_width(self):
        width = self.scene.get_property('width')
        return format_float(width)

    def model_depth(self):
        depth = self.scene.get_property('depth')
        return format_float(depth)

    def model_height(self):
        height = self.scene.get_property('height')
        return format_float(height)

    def model_rotation_x(self):
        angle = self.scene.get_property('rotation-x')
        return format_float(angle)

    def model_rotation_y(self):
        angle = self.scene.get_property('rotation-y')
        return format_float(angle)

    def model_rotation_z(self):
        angle = self.scene.get_property('rotation-z')
        return format_float(angle)

    @property
    def current_dir(self):
        """
        Return path where a file should be saved to.
        """
        if self.model_file is not None:
            dur = self.model_file.dirname
        elif len(self.recent_files) > 0:
            dur = os.path.dirname(self.recent_files[0][1])
        else:
            dur = os.getcwd()

        return dur

    def command_line(self):
        if len(sys.argv) > 1:
            self.open_and_display_file(os.path.abspath(sys.argv[1]))

    # -------------------------------------------------------------------------
    # EVENT HANDLERS
    # -------------------------------------------------------------------------

    def on_file_open(self, event=None):
        if self.save_changes_dialog():
            show_again = True

            while show_again:
                dialog = OpenDialog(self.window, self.current_dir)
                fpath = dialog.get_path()
                if fpath:
                    show_again = not self.open_and_display_file(fpath, dialog.get_type())
                else:
                    show_again = False

    def on_file_save(self, event=None):
        """
        Save changes to the same file.
        """
        self.scene.export_to_file(self.model_file)
        self.window.file_modified = False

    def on_file_save_as(self, event=None):
        """
        Save changes to a new file.
        """
        dialog = SaveDialog(self.window, self.current_dir)
        fpath = dialog.get_path()
        if fpath:
            stl_file = ModelFile(fpath)
            self.scene.export_to_file(stl_file)
            self.model_file = stl_file
            self.window.filename = stl_file.basename
            self.window.file_modified = False

    def on_quit(self, event=None):
        """
        On quit, write config settings and show a dialog proposing to save the
        changes if the scene has been modified.
        """
        try:
            self.config.write('ui.recent_files', os.path.pathsep.join([f[1] + str(OpenDialog.ftypes.index(f[2]))
                for f in self.recent_files]))

            w, h = self.window.get_size()
            self.config.write('ui.window_w', w)
            self.config.write('ui.window_h', h)

            if self.scene:
                self.config.write('ui.gcode_2d', int(self.scene.mode_2d))

            self.config.commit()
        except IOError:
            logging.warning('Could not write settings to config file %s' % self.config.fname)

        if self.save_changes_dialog():
            self.window.quit()

    def save_changes_dialog(self):
        proceed = True
        if self.scene and self.scene.model_modified:
            ask_again = True

            while ask_again:
                dialog = QuitDialog(self.window)
                response = dialog.show()
                if response == QuitDialog.RESPONSE_SAVE:
                    self.on_file_save()
                    ask_again = False
                elif response == QuitDialog.RESPONSE_SAVE_AS:
                    self.on_file_save_as()
                    ask_again = self.scene.model_modified
                elif response == QuitDialog.RESPONSE_CANCEL:
                    ask_again = False
                    proceed = False
                elif response == QuitDialog.RESPONSE_DISCARD:
                    ask_again = False
                else:
                    logging.warning('Unknown dialog response: %s' % response)
                    ask_again = False
                    proceed = False

        return proceed

    def on_about(self, event=None):
        AboutDialog()

    def scaling_factor_changed(self, factor):
        try:
            self.scene.scale_model(float(factor))
            self.scene.invalidate()
            # tell all the widgets that care about model size that it has changed
            self.panel.model_size_changed()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass # ignore invalid values

    def dimension_changed(self, dimension, value):
        try:
            self.scene.change_model_dimension(dimension, float(value))
            self.scene.invalidate()
            self.panel.model_size_changed()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass # ignore invalid values

    def on_layers_changed(self, layers):
        self.scene.change_num_layers(layers)
        self.scene.invalidate()

    def rotation_changed(self, axis, angle):
        try:
            self.scene.rotate_model(float(angle), axis)
            self.scene.invalidate()
            self.window.file_modified = self.scene.model_modified
        except ValueError:
            pass # ignore invalid values

    def on_center_model(self):
        """
        Center model on platform.
        """
        self.scene.center_model()
        self.scene.invalidate()
        self.window.file_modified = self.scene.model_modified

    def on_arrows_toggled(self, value):
        """
        Show/hide arrows on the Gcode model.
        """
        self.scene.show_arrows(value)
        self.scene.invalidate()

    def on_reset_view(self):
        """
        Restore the view of the model shown on startup.
        """
        self.scene.reset_view()
        self.scene.invalidate()

    def on_set_mode(self, value):
        self.scene.mode_2d = not value
        if self.scene.initialized:
            self.scene.invalidate()

    def on_set_ortho(self, value):
        self.scene.mode_ortho = value
        self.scene.invalidate()

    def on_view_front(self):
        self.scene.rotate_view(0, 0)

    def on_view_back(self):
        self.scene.rotate_view(180, 0)

    def on_view_left(self):
        self.scene.rotate_view(90, 0)

    def on_view_right(self):
        self.scene.rotate_view(-90, 0)

    def on_view_top(self):
        self.scene.rotate_view(0, -90)

    def on_view_bottom(self):
        self.scene.rotate_view(0, 90)

    # -------------------------------------------------------------------------
    # FILE OPERATIONS
    # -------------------------------------------------------------------------

    def update_recent_files(self, fpath, ftype=None):
        self.recent_files = [f for f in self.recent_files if f[1] != fpath]
        self.recent_files.insert(0, (os.path.basename(fpath), fpath, ftype))
        self.recent_files = self.recent_files[:self.RECENT_FILE_LIMIT]
        self.window.update_recent_files_menu(self.recent_files)

    def open_and_display_file(self, fpath, ftype=None):
        self.set_wait_cursor()
        progress_dialog_read = None
        progress_dialog_load = None
        success = True

        try:
            self.update_recent_files(fpath, ftype)
            self.model_file = ModelFile(fpath, ftype)

            self.scene = Scene(self.window)

            progress_dialog_read = ProgressDialog('Reading file...')
            model, model_data = self.model_file.read(progress_dialog_read.step)

            progress_dialog_load = ProgressDialog('Loading model...')
            model.load_data(model_data, progress_dialog_load.step)

            self.scene.clear()
            self.scene.add_model(model)

            if self.model_file.filetype == 'gcode':
                offset_x = self.config.read('machine.platform_offset_x', float)
                offset_y = self.config.read('machine.platform_offset_y', float)
                offset_z = self.config.read('machine.platform_offset_z', float)

                if offset_x is None and offset_y is None and offset_z is None:
                    self.scene.view_model_center()
                    logging.info('Platform offsets not set, showing model in the center')
                else:
                    model.offset_x = offset_x if offset_x is not None else 0
                    model.offset_y = offset_y if offset_y is not None else 0
                    model.offset_z = offset_z if offset_z is not None else 0
                    logging.info('Using platform offsets: (%s, %s, %s)' % (
                        model.offset_x, model.offset_y, model.offset_z))

            # platform needs to be added last to be translucent
            platform_w = self.config.read('machine.platform_w', float)
            platform_d = self.config.read('machine.platform_d', float)
            platform = Platform(platform_w, platform_d)
            self.scene.add_supporting_actor(platform)

            self.panel = self.create_panel()
            # update panel to reflect new model properties
            self.panel.set_initial_values()
            self.panel.connect_handlers()

            # always start with the same view on the scene
            self.scene.reset_view(True)
            if self.model_file.filetype == 'gcode':
                self.scene.mode_2d = bool(self.config.read('ui.gcode_2d', int))
            else:
                self.scene.mode_2d = False

            if hasattr(self.panel, 'set_3d_view'):
                self.panel.set_3d_view(not self.scene.mode_2d)

            self.window.set_file_widgets(self.scene, self.panel)
            self.window.filename = self.model_file.basename
            self.window.file_modified = False
            self.window.menu_enable_file_items(self.model_file.filetype != 'gcode')


            if self.model_file.size > 2**30:
                size = self.model_file.size / 2**30
                units = 'GB'
            elif self.model_file.size > 2**20:
                size = self.model_file.size / 2**20
                units = 'MB'
            elif self.model_file.size > 2**10:
                size = self.model_file.size / 2**10
                units = 'KB'
            else:
                size = self.model_file.size
                units = 'B'

            vertex_plural = 'vertex' if int(str(model.vertex_count)[-1]) == 1 else 'vertices'
            self.window.update_status(' %s (%.1f%s, %d %s)' % (
                self.model_file.basename, size, units, model.vertex_count, vertex_plural))
        except IOError, e:
            self.set_normal_cursor()
            error_dialog = OpenErrorAlert(fpath, e.strerror)
            error_dialog.show()
            success = False
        except ModelFileError, e:
            self.set_normal_cursor()
            error_dialog = OpenErrorAlert(fpath, e.message)
            error_dialog.show()
            success = False