Example #1
0
class FileChooser(Gtk.Bin):
    __gtype_name__ = 'FileChooser'
    __gsignals__ = {
        'file-activated': (GObject.SignalFlags.RUN_FIRST, None, (str, )),
        'selection-changed': (GObject.SignalFlags.RUN_FIRST, None, (str, ))
    }

    def __init__(self, widget_window):
        Gtk.Bin.__init__(self)

        self.widget_window = widget_window

        # Glade setup
        self.builder = Gtk.Builder()
        self.builder.add_from_file(os.path.join(UIDIR, "filechooser.glade"))
        self.builder.connect_signals(self)

        self.builder.get_object = self.builder.get_object

        self.add(self.builder.get_object('filechooser'))

        # Retrieve frequently used objects
        self.nav_box = self.builder.get_object('nav_box')
        self.nav_btn_box = self.builder.get_object('nav_btn_box')

        file_adj = self.builder.get_object('fileview')
        self.file_vadj = file_adj.get_vadjustment()
        self.file_hadj = file_adj.get_hadjustment()

        # Retrieve data models
        self.file_liststore = self.builder.get_object("file_liststore")

        # Retrieve treeviews
        self.file_treeview = self.builder.get_object("file_treeview")
        self.bookmark_listbox = self.builder.get_object("bookmark_listbox")

        # Enable DnD ToDo implement DnD
        self.file_treeview.enable_model_drag_source(
            Gdk.ModifierType.BUTTON1_MASK, [('text/plain', 0, 0)],
            Gdk.DragAction.MOVE | Gdk.DragAction.COPY)
        self.file_treeview.enable_model_drag_dest([('text/plain', 0, 0)],
                                                  Gdk.DragAction.COPY)

        # Connect callbacks to VolumeMonitor
        self.mounts = Gio.VolumeMonitor.get()
        self.mounts.connect('mount-added', self.on_mount_added)
        self.mounts.connect('mount-removed', self.on_mount_removed)

        # Initialize helpers
        self.bookmarks = BookMarks()
        self.icons = Icons(Gtk.IconTheme.get_default())

        # Initialize places
        self.userdirs = UserDirectories()
        desktop = self.userdirs.get_XDG_directory('XDG_DESKTOP_DIR')
        home = self.userdirs.get_home_directory()
        self.places = [home, desktop]

        # Initialize variables
        self._cur_dir = None
        self._old_dir = ''
        self._filters = {}
        self._filter = None
        self._files = []
        self._show_hidden = False
        self._hidden_exts = ['.desktop']
        self._copy = True
        self._selection = None
        self.nav_btn_list = []
        self.nav_btn_path_dict = {}
        self.eject_btn_path_dict = {}

        # Initialize
        self._init_nav_buttons()

        self.show_all()

    # Have to do this once realized so sizes will have been allocated
    def on_filechooser_realize(self, widget):
        self._update_bookmarks()
        self._fill_file_liststore(self._cur_dir)

    def _init_nav_buttons(self):
        # FixMe this needs a lot of work. Try to copy this implementation
        # https://searchcode.com/codesearch/view/22668315/
        box = self.nav_btn_box
        box.get_style_context().add_class(Gtk.STYLE_CLASS_LINKED)

        arrow_left = Gtk.Arrow.new(Gtk.ArrowType.LEFT, Gtk.ShadowType.NONE)
        self.arrow_btn_left = Gtk.Button()
        self.arrow_btn_left.add(arrow_left)
        box.add(self.arrow_btn_left)

        self.btn_goto_root = Gtk.Button.new_from_icon_name(
            'gtk-harddisk', Gtk.IconSize.LARGE_TOOLBAR)
        self.btn_goto_root.connect('pressed', self.on_goto_root_clicked)
        box.add(self.btn_goto_root)

        btn_list = self.nav_btn_list
        btn_dict = self.nav_btn_path_dict
        for i in range(10):
            btn = Gtk.Button()
            btn.set_hexpand(False)
            btn.connect('clicked', self.on_nav_btn_clicked)
            btn.set_can_focus(False)
            btn.set_use_underline(False)
            btn_list.append(btn)
            box.add(btn)
            btn_dict[btn] = ''

#        arrow_right = Gtk.Arrow.new(Gtk.ArrowType.RIGHT, Gtk.ShadowType.NONE)
#        self.arrow_btn_right = Gtk.Button()
#        self.arrow_btn_right.add(arrow_right)
#        box.add(self.arrow_btn_right)

        box.show_all()

    def _update_nav_buttons(self, path=None):
        for btn in self.nav_btn_list:
            btn.hide()
        if path is None:
            path = self._cur_dir
        if len(path) == 1:
            return
        places = path.split('/')[1:]
        path = '/'
        w_needed = 0
        w_allowed = self.nav_btn_box.get_allocated_width()
        for i, place in enumerate(places):
            btn = self.nav_btn_list[i]
            btn.set_label(place)
            path = os.path.join(path, place)
            self.nav_btn_path_dict[btn] = path
            btn.show()
            w_needed += btn.get_allocated_width()
        if w_needed > w_allowed:
            self.btn_goto_root.hide()
            self.arrow_btn_left.show()
        else:
            self.btn_goto_root.show()
            self.arrow_btn_left.hide()
        count = 0
        while w_needed > w_allowed:
            btn = self.nav_btn_list[count]
            w_needed -= btn.get_allocated_width()
            btn.hide()
            count += 1

    def on_nav_btn_clicked(self, widget, data=None):
        path = self.nav_btn_path_dict[widget]
        if path != self._cur_dir:
            self._fill_file_liststore(path)

    def on_goto_root_clicked(self, widget, data=None):
        self._fill_file_liststore('/')

    # HACK for now
    def on_arrow_left_clicked(self, widget, data=None):
        self.up_one_dir()

    def on_arrow_right_clicked(self, widget, data=None):
        pass

    def _fill_file_liststore(self, path=None):
        model = self.file_liststore
        model.clear()
        self.selected_row = None

        if path:
            self._cur_dir = path
            # Reset scrollbars since display has changed
            self.file_vadj.set_value(0)
            self.file_hadj.set_value(0)
        if self._cur_dir is None:
            self._cur_dir = self.userdirs.get_XDG_directory('XDG_DESKTOP_DIR')

        files = []
        folders = []
        if self._filter:
            exts = self._filters[self._filter]
        else:
            exts = '*'  # Don't filter
        names = os.listdir(self._cur_dir)
        for name in names:
            if name[0] == '.' and not self._show_hidden:
                continue
            path = os.path.join(self._cur_dir, name)
            if os.path.islink(path):
                path = os.readlink(path)
            if os.path.isdir(path):
                folders.append(name)
            elif os.path.isfile(path):
                ext = os.path.splitext(name)[1]
                if '*' in exts or ext in exts and not ext in self._hidden_exts:
                    files.append(name)

        folders.sort(key=str.lower, reverse=False)
        for fname in folders:
            fpath = os.path.join(self._cur_dir, fname)
            icon = self.icons.get_for_directory(fpath)
            model.append([0, icon, fname, None, None])

        files.sort(key=str.lower, reverse=False)
        for fname in files:
            fpath = os.path.join(self._cur_dir, fname)
            icon = self.icons.get_for_file(fname)
            size, date = self._get_file_data(fpath)
            model.append([0, icon, fname, size, date])

        self._update_nav_buttons()

        self.emit('selection-changed', self._cur_dir)

        # If dir is in bookmarks, select the bookmark
        for row in self.bookmark_listbox.get_children():
            bookmark_path = row.get_tooltip_text()
            if bookmark_path == self._cur_dir:
                self.bookmark_listbox.select_row(row)
                break
        else:
            self.bookmark_listbox.unselect_all()

        # No file selected yet, so desensitize edit buttons
        self.builder.get_object('edit_button').set_sensitive(False)
        self.builder.get_object('cut_button').set_sensitive(False)
        self.builder.get_object('copy_button').set_sensitive(False)
        self.builder.get_object('delete_button').set_sensitive(False)

    def _get_file_data(self, fpath):
        size = os.path.getsize(fpath)
        if size >= 1E9:
            size_str = "{:.1f} GB".format(size / 1E9)
        elif size >= 1E6:
            size_str = "{:.1f} MB".format(size / 1E6)
        elif size >= 1E3:
            size_str = "{:.1f} KB".format(size / 1E3)
        else:
            size_str = "{} bytes".format(size)
        tstamp = os.path.getmtime(fpath)
        date_str = datetime.fromtimestamp(tstamp).strftime("%m/%d/%y %X")
        return size_str, date_str

    def count_lines(self, filename):
        lines = 0
        buf_size = 1024 * 1024
        with open(filename) as fh:
            read_f = fh.read
            buf = read_f(buf_size)
            while buf:
                lines += buf.count('\n')
                buf = read_f(buf_size)
        return lines + 1

    def on_select_toggled(self, widget, path):
        model = self.file_liststore
        model[path][0] = not model[path][0]

    def on_file_treeview_key_press_event(self, widget, event):
        kv = event.keyval
        # Events that don't need to know about modifiers
        if kv == Gdk.KEY_Escape:
            self.file_treeview.get_selection().unselect_all()
            self.builder.get_object('edit_button').set_sensitive(False)
            self.builder.get_object('cut_button').set_sensitive(False)
            self.builder.get_object('copy_button').set_sensitive(False)
            self.builder.get_object('delete_button').set_sensitive(False)
            return True
        elif kv == Gdk.KEY_Delete:
            self.delete_selected()
            return True
        elif kv == Gdk.KEY_F2:
            self.edit_selected()
            return True

        # Handle other events
        # Determine the actively pressed modifier
        modifier = event.get_state() & Gtk.accelerator_get_default_mod_mask()

        # Bool of Control or Shift modifier states
        control = modifier == Gdk.ModifierType.CONTROL_MASK
        shift = modifier == Gdk.ModifierType.SHIFT_MASK

        if control and kv == Gdk.KEY_c:
            return self.copy_selected()
        elif control and kv == Gdk.KEY_x:
            return self.cut_selected()
        elif control and kv == Gdk.KEY_v:
            return self.paste()

    def on_filechooser_treeview_cursor_changed(self, widget):
        row = widget.get_cursor()[0]
        # Prevent emitting selection changed on double click
        if row == self.selected_row or row is None:
            return

        self.selected_row = row

        fname = self.file_liststore[row][2]
        fpath = os.path.join(self._cur_dir, fname)
        self.emit('selection-changed', fpath)

        self.builder.get_object('edit_button').set_sensitive(True)
        self.builder.get_object('cut_button').set_sensitive(True)
        self.builder.get_object('copy_button').set_sensitive(True)
        self.builder.get_object('delete_button').set_sensitive(True)

    def on_filechooser_treeview_row_activated(self, widget, path, colobj):
        fname = self.file_liststore[path][2]
        fpath = os.path.join(self._cur_dir, fname)
        if os.path.isfile(fpath):
            self.emit('file-activated', fpath)
        elif os.path.isdir(fpath):
            self._fill_file_liststore(fpath)
        else:
            # If neither, probably does not exist, so reload
            self._fill_file_liststore()

    def on_file_name_editing_started(self, renderer, entry, row):
        keyboard.show(entry)

    def on_file_name_edited(self, widget, row, new_name):
        model = self.file_liststore
        model[row][0] = 0
        old_name = model[row][2]
        old_path = os.path.join(self._cur_dir, old_name)
        new_path = os.path.join(self._cur_dir, new_name)
        if old_name == new_name:
            return
        if not os.path.exists(new_path):
            os.rename(old_path, new_path)
            msg = 'Renamed "{}" to "{}"'.format(old_name, new_name)
            log.info(msg)
            self.widget_window.show_info(msg, 2)
            model[row][2] = new_name
        else:
            msg = "Destination file already exists, won't rename"
            log.warning(msg)
            self.widget_window.show_warning(msg)

    # =======================================
    #   Methods to be called externally
    # =======================================

    # Add filter by name
    def add_filter(self, name, exts):
        self._filters[name] = [ext.replace('*.', '.') for ext in exts]

    # Delete filter by name
    def delete_filter(self, name):
        if name in self._filters:
            del self._filters[name]
            if self._filter == name:
                self._filter = None
            return True
        return False

    # Set current filter by name
    def set_filter(self, name):
        if name is None:
            self._filter = None
        elif name in self._filters:
            self._filter = name
        self._fill_file_liststore()

    # Get current filter
    def get_filter(self):
        if self._filter in self._filters:
            return ['*' + ext for ext in self._filters[self._filter]]
        return None

    # Get names of all specified filters
    def get_filters(self):
        filters = []
        for filter in self._filters:
            filters.append(filter)
        return filters

    # Get whether hidden files are shown
    def get_show_hidden(self):
        return self._show_hidden

    # Set whether hidden files are shown
    def set_show_hidden(self, setting):
        self._show_hidden = setting

    # Get the path of the current display directory
    def get_current_folder(self):
        return self._cur_dir

    # Set current display directory to path
    def set_current_folder(self, fpath):
        #        fpath = fpath.rstrip('/')
        if os.path.exists(fpath):
            self._fill_file_liststore(fpath)
            log.info('Setting the current folder to "{}"'.format(fpath))
            return True
        log.error(
            'Can not set current folder to "{}", folder does not exist'.format(
                fpath))
        return False

    # Get absolute path at cursor
    def get_path_at_cursor(self):
        path = self.file_treeview.get_cursor()[0]
        if path is not None:
            fname = self.file_liststore[path][2]
            fpath = os.path.join(self._cur_dir, fname)
            return fpath
        return None

    # Set cursor at path
    def set_cursor_at_path(self, fpath):
        model = self.file_liststore
        tree = self.file_treeview
        if not os.path.exists(fpath):
            return False
        fpath, fname = os.path.split(fpath)
        if fpath != self._cur_dir:
            self._fill_file_liststore(fpath)
        for row in range(len(model)):
            if model[row][2] == fname:
                tree.set_cursor(row)
                return True
        return False

    # Get paths for selected
    def get_selected(self):
        model, rows = self.file_treeview.get_selection().get_selected_rows()
        if not rows:
            return
        paths = []
        for row in rows:
            fpath = os.path.join(self._cur_dir, model[row.to_string()][2])
            paths.append(fpath)
        return paths

    # Check checkbox at file path
    def set_selected(self, fpath):
        model = self.file_liststore
        tree = self.file_treeview
        if not os.path.exists(fpath):
            return False
        fpath, fname = os.path.split(fpath)
        if fpath != self._cur_dir:
            self._fill_file_liststore(fpath)
        for row in range(len(model)):
            if model[row][2] == fname:
                model[row][0] = 1
                return True
        return False

    # Check all checkboxes in current display directory
    def select_all(self, fpath=None):
        model = self.file_liststore
        if fpath is not None:
            if os.path.isdir(fpath):
                self._fill_file_liststore(fpath)
            else:
                return False
        for row in range(len(model)):
            model[row][0] = 1
        return True

    # Uncheck all checkboxes in current display directory
    def unselect_all(self):
        model = self.file_liststore
        for row in range(len(model)):
            model[row][0] = 0

    # Get paths to current mounts
    def get_mounts(self):
        mounts = self.mounts.get_mounts()
        paths = []
        for mount in mounts:
            path = mount.get_root().get_path()
            name = mount.get_name()
            paths.append([path, name, mount])
        return paths

    # Get list of user bookmarks
    def get_bookmarks(self):
        return self.bookmarks.get()

    # Add bookmark
    def add_bookmark(self, path):
        if not path in self.places:
            self.bookmarks.add(path)
            self._update_bookmarks()

    # Remove bookmark
    def remove_bookmark(self, path):
        if not path in self.places:
            self.bookmarks.remove(path)
            self._update_bookmarks()

    # Clear all bookmarks
    def clear_bookmarks(self):
        self.bookmarks.clear()
        self._update_bookmarks()

    # Display parent of current directory
    def up_one_dir(self):
        path = os.path.dirname(self._cur_dir)
        self._fill_file_liststore(path)

    # Cut selected files
    def cut_selected(self, widegt=None, data=None):
        files = self.get_selected()
        if not files:
            log.error("No files selected to cut")
            return False
        self._files = files
        self._copy = False
        log.debug("Files to cut: {}".format(files))
        self.builder.get_object('paste_button').set_sensitive(True)
        return True

    # Copy selected files
    def copy_selected(self, widegt=None, data=None):
        files = self.get_selected()
        if files is None:
            log.error("No files selected to copy")
            return False
        self._files = files
        self._copy = True
        log.debug("Files to copy: {}".format(files))
        self.builder.get_object('paste_button').set_sensitive(True)
        return True

    # Paste previously cut/copied files to current directory
    def paste(self, widegt=None, data=None):
        src_list = self._files
        if src_list is None:
            return False
        dst_dir = self._cur_dir
        if self._copy:
            for src in self._files:
                self._copy_file(src, dst_dir)
        elif not self._copy:
            for src in self._files:
                self._move_file(src, dst_dir)
            self._files = None
        self._fill_file_liststore()
        self.builder.get_object('paste_button').set_sensitive(False)
        return True

    # Save file as, if path is specified it will be saved in that directory
    def save_as(self, path=None):
        model = self.file_liststore
        tree = self.file_treeview
        if path is None:
            path = self.get_selected()[0]
        if not os.path.exists(path) or path is None:
            return False
        fpath, fname = os.path.split(path)
        new_name = self._copy_file(path, fdir)
        for row in range(len(model)):
            if model[row][2] == new_name:
                break
        focus_column = self.builder.get_object('col_file_name')
        model[row][0] = 1
        tree.set_cursor(row, focus_column, True)

    # Create a new folder in the current directory
    def new_folder(self, widegt=None, data=None):
        model = self.file_liststore
        tree = self.file_treeview

        name = "New Folder"
        count = 1
        while os.path.exists(os.path.join(self._cur_dir, name)):
            name = 'New Folder {0}'.format(count)
            count += 1

        path = os.path.join(self._cur_dir, name)
        os.makedirs(path)

        self._fill_file_liststore()

        for row in range(len(model)):
            if model[row][2] == name:
                break
        focus_column = self.builder.get_object('col_file_name')
        model[row][0] = 1
        tree.set_cursor(row, focus_column, True)

    # Create a new folder in the current directory
    def new_file(self, widegt=None, data=None):
        model = self.file_liststore
        tree = self.file_treeview

        name = "New file"
        count = 1
        while os.path.exists(os.path.join(self._cur_dir, name)):
            name = 'New Folder {0}'.format(count)
            count += 1

        path = os.path.join(self._cur_dir, name)
        with open(path, 'w') as fh:
            pass
        self._fill_file_liststore()

        for row in range(len(model)):
            if model[row][2] == name:
                break
        focus_column = self.builder.get_object('col_file_name')
        model[row][0] = 1
        tree.set_cursor(row, focus_column, True)

    def edit_selected(self, widget=None, data=None):
        row = self.file_treeview.get_cursor()[0]
        if not row:
            return
        model = self.file_liststore
        tree = self.file_treeview
        focus_column = self.builder.get_object('col_file_name')
        model[row][0] = 1
        tree.set_cursor(row, focus_column, True)

    # Move selected files to trash (see file_util code at end)
    def delete_selected(self, widegt=None, data=None):
        paths = self.get_selected()
        if paths is None:
            return
        num = len(paths)
        for path in paths:
            result, msg = move2trash(path)
        self._fill_file_liststore()

        if result == 'INFO':
            self.widget_window.show_info(msg, 2)
        else:
            self.widget_window.show_error(msg)

    # =======================================
    #   Drag and Drop TODO
    # =======================================

    def on_file_treeview_drag_begin(self, data, som):
        log.debug("drag {0} {1}".format(data, som))

    def on_file_treeview_drag_data_received(self):
        log.debug("got drag")

    def drag_data_received_cb(self, treeview, context, x, y, selection, info,
                              timestamp):
        drop_info = treeview.get_dest_row_at_pos(x, y)
        log.debug("got drag")
        if drop_info:
            model = treeview.get_model()
            path, position = drop_info
            data = selection.data
            # do something with the data and the model
            log.debug("{0} {1} {2}".format(model, path, data))
        return

    # =======================================
    #   Bookmark treeview
    # =======================================

    def on_mount_added(self, volume, mount):
        path = mount.get_root().get_path()
        name = os.path.split(path)[1]
        msg = 'External storage device "{}" mounted'.format(name)
        log.info(msg)
        self.widget_window.show_info(msg, 2)
        self._update_bookmarks()

    def on_mount_removed(self, volume, mount):
        path = mount.get_root().get_path()
        name = os.path.split(path)[1]
        msg = 'External storage device "{}" removed'.format(name)
        log.info(msg)
        self.widget_window.show_info(msg, 2)
        if self._cur_dir.startswith(path):
            self.file_liststore.clear()
            self._update_nav_buttons('')
        self._update_bookmarks()

    def on_eject_clicked(self, widget):
        mount = self.eject_btn_path_dict[widget]
        path = mount.get_root().get_path()
        name = mount.get_name()
        mount.eject(0, None, self.on_eject_finished)

    def on_eject_finished(self, mount, result):
        msg = 'Safe to remove external storage device "{}"'.format(
            mount.get_name())
        log.info(msg)
        self.widget_window.show_info(msg, 2)

    def on_add_bookmark_button_release_event(self, widget, data=None):
        path = self.file_treeview.get_cursor()[0]
        if path is None:
            fpath = self._cur_dir
        else:
            fname = self.file_liststore[path][2]
            fpath = os.path.join(self._cur_dir, fname)
        if not os.path.isdir(fpath):
            return
        self.add_bookmark(fpath)

    def on_remove_bookmark_button_release_event(self, widget, data=None):
        row = self.bookmark_listbox.get_selected_row()
        if row is None:
            return
        path = row.get_tooltip_text()
        self.bookmark_listbox.remove(row)
        self.bookmarks.remove(path)

    def _update_bookmarks(self):
        ext_media = sorted(self.get_mounts(), key=self.sort, reverse=False)
        bookmarks = sorted(self.bookmarks.get(), key=self.sort, reverse=False)

        for child in self.bookmark_listbox.get_children():
            self.bookmark_listbox.remove(child)

        # Add the places
        for path in self.places:
            icon = self.icons.get_for_directory(path)
            self.add_listbox_row(icon, path)

        # Add the mounts
        self.eject_btn_path_dict = {}
        icon = self.icons.get_for_device('USBdrive')
        for device in ext_media:
            path, name, mount = device
            if mount.can_eject():
                self.add_listbox_row(icon, path, name, mount)
            else:
                self.add_listbox_row(icon, path, name)

        # Add the seperator
        row = Gtk.ListBoxRow()
        row.set_selectable(False)
        separator = Gtk.Separator()
        row.add(separator)
        self.bookmark_listbox.add(row)

        # Add the bookmarks
        for bookmark in bookmarks:
            path, name = bookmark
            if not os.path.exists(path):
                continue
            icon = self.icons.get_for_directory(path)
            self.add_listbox_row(icon, path, name)

        self.bookmark_listbox.show_all()

    def add_listbox_row(self, icon, path, name=None, mount=None):
        if not name or name == '':
            name = os.path.split(path)[1]
        row = Gtk.ListBoxRow()
        row.set_tooltip_text(path)
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0)
        row.add(hbox)

        # Add icon
        image = Gtk.Image.new_from_pixbuf(icon)
        hbox.pack_start(image, False, False, 0)

        # Add label
        label = Gtk.Label()
        label.set_text(name)
        label.set_xalign(0)
        hbox.pack_start(label, True, True, 4)

        # Add media eject button
        if mount is not None:
            icon = self.icons.get_for_device('media-eject')
            image = Gtk.Image.new_from_pixbuf(icon)
            btn = Gtk.Button()
            self.eject_btn_path_dict[btn] = mount
            btn.connect('clicked', self.on_eject_clicked)
            btn.set_name('eject')
            btn.set_image(image)
            hbox.pack_start(btn, False, False, 0)

        self.bookmark_listbox.add(row)

    # Generate sort key based on file basename
    def sort(self, location):
        path = location[0]
        name = location[1]
        if name is None:
            return os.path.basename(path).lower()
        return name

    def on_bookmark_activated(self, widget, data=None):
        path = data.get_tooltip_text()
        self._fill_file_liststore(path)

    # =======================================
    #   File utilities
    # =======================================

    def _copy_file(self, src, dst_dir):
        src_dir, src_name = os.path.split(src)
        dst_name = src_name

        # find a unique copy name
        if src_dir == dst_dir:
            name, ext = os.path.splitext(dst_name)
            if '_copy' in name:
                name = name.rpartition('_copy')[0]

            count = 1
            while os.path.exists(os.path.join(dst_dir, dst_name)):
                dst_name = '{0}_copy{1}{2}'.format(name, count, ext)
                count += 1

        dst = os.path.join(dst_dir, dst_name)

        if os.path.exists(dst):
            text = "Destination already exists! \n Overwrite {}?".format(
                dst_name)
            overwrite = False  #self.ok_cancel_dialog.run(text)
            if not overwrite:
                msg = "User selected not to overwrite {}".format(dst_name)
                log.info(msg)
                self.widget_window.show_info(msg, 2)
                return

        if os.path.isfile(src):
            shutil.copy2(src, dst)

        else:
            shutil.copytree(src, dst)

        msg = 'Copied "{0}" to "{1}"'.format(src_name, dst_dir)
        log.info(msg)
        self.widget_window.show_info(msg, 2)

        self._fill_file_liststore()
        return dst_name

    def _move_file(self, src, dst_dir):
        src_dir, src_name = os.path.split(src)

        if src_dir == dst_dir:
            msg = "MOVE ERROR: Source and destination are the same"
            log.error(msg)
            self.widget_window.show_error(msg)
            return

        dst = os.path.join(dst_dir, src_name)

        if os.path.exists(dst):
            msg = "WARNING: Destination already exists. Overwrite {}?".format(
                src_name)
            overwrite = False  #self.ok_cancel_dialog.run(text)
            if not overwrite:
                msg = 'User selected not to overwrite "{}"'.format(src_name)
                log.info(msg)
                self.widget_window.show_info(msg, 2)
                return

        msg = 'Moving "{0}" to "{1}"'.format(src_name, dst_dir)
        log.info(msg)
        self.widget_window.show_info(msg, 2)
        shutil.move(src, dst)
Example #2
0
class Filechooser(gobject.GObject):
    __gtype_name__ = 'Filechooser'
    __gsignals__ = {
        'file-activated': (gobject.SIGNAL_RUN_FIRST, None, (str, )),
        'selection-changed': (gobject.SIGNAL_RUN_FIRST, None, (str, )),
        'filename-editing-started':
        (gobject.SIGNAL_RUN_FIRST, None, (object, )),
        'button-release-event': (gobject.SIGNAL_RUN_FIRST, None, ()),
        'error': (gobject.SIGNAL_RUN_FIRST, None, (str, str))
    }

    def __init__(self):

        gobject.GObject.__init__(self)

        # Glade setup
        self.builder = gtk.Builder()
        self.builder.add_from_file(os.path.join(UIDIR, "filechooser.glade"))
        self.builder.connect_signals(self)

        # Retrieve frequently used objects
        self.nav_box = self.builder.get_object('hbox1')
        self.eject_column = self.builder.get_object('eject_col')
        file_adj = self.builder.get_object('scrolledwindow1')
        self.file_vadj = file_adj.get_vadjustment()
        self.file_hadj = file_adj.get_hadjustment()

        # Retrieve data models
        self.file_liststore = self.builder.get_object("file_liststore")
        self.bookmark_liststore = self.builder.get_object("bookmark_liststore")

        # Retrieve treeviews
        self.file_treeview = self.builder.get_object("file_treeview")
        self.bookmark_treeview = self.builder.get_object("bookmark_treeview")

        self.bookmark_treeview.set_row_separator_func(self.bookmark_separator)

        # Enable DnD TODO DnD is not implemented yet
        self.file_treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, \
                                                    [('text/plain', 0, 0)], gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_COPY)
        self.file_treeview.enable_model_drag_dest([('text/plain', 0, 0)], \
                                                  gtk.gdk.ACTION_COPY)

        # Connect callbacks to VolumeMonitor
        self.mounts = gio.VolumeMonitor()
        self.mounts.connect('mount-added', self.on_mount_added)
        self.mounts.connect('mount-removed', self.on_mount_removed)

        # Initialize objects
        self.ok_cancel_dialog = Dialogs(DialogTypes.OK_CANCEL)
        self.bookmarks = BookMarks()
        self.icons = Icons(gtk.icon_theme_get_default())

        # Initialize places
        home = os.environ['HOME']
        desktop = os.path.expanduser("~/Desktop")
        self.places = [home, desktop]

        # Initialize variables
        self._cur_dir = desktop
        self._old_dir = " "
        self._filters = {}
        self._filter = ''
        self._files = []
        self._show_hidden = False
        self._hidden_exts = ['.desktop']
        self._copy = True
        self._selection = None
        self.nav_btn_list = []
        self.nav_btn_path_dict = {}

        # Initialize
        self._init_nav_buttons()

    # Have to do this once realized so sizes will have been allocated
    def on_vbox1_realize(self, widget):
        self._update_bookmarks()
        self._fill_file_liststore(self._cur_dir)

    def _init_nav_buttons(self):
        box = self.nav_box
        btn_list = self.nav_btn_list
        btn_dict = self.nav_btn_path_dict
        for i in range(10):
            btn = gtk.Button()
            btn.connect('clicked', self.on_nav_btn_clicked)
            btn.set_can_focus(False)
            btn.set_use_underline(False)
            btn_list.append(btn)
            box.pack_start(btn, False, False, 0)
            btn_dict[btn] = ''

    def _update_nav_buttons(self, path=None):
        if path is None:
            path = self._cur_dir
        places = path.split('/')[1:]
        path = '/'
        for btn in self.nav_btn_list:
            btn.hide()
        w_needed = 0
        for i, place in enumerate(places):
            btn = self.nav_btn_list[i]
            btn.set_label(place)
            path = os.path.join(path, place)
            self.nav_btn_path_dict[btn] = path
            btn.show()
            w_needed += btn.size_request()[0]
        w_allowed = self.nav_box.get_allocation()[2]
        if w_needed > w_allowed:
            self.builder.get_object('goto_root').hide()
            self.builder.get_object('arrow_left').show()
        else:
            self.builder.get_object('goto_root').show()
            self.builder.get_object('arrow_left').hide()
        count = 0
        while w_needed > w_allowed:
            btn = self.nav_btn_list[count]
            w_needed -= btn.size_request()[0]
            btn.hide()
            count += 1

    def on_nav_btn_clicked(self, widget, data=None):
        path = self.nav_btn_path_dict[widget]
        if path != self._cur_dir:
            self._fill_file_liststore(path)

    def on_goto_root_clicked(self, widget, data=None):
        self._fill_file_liststore('/')

    # HACK for now
    def on_arrow_left_clicked(self, widget, data=None):
        self.up_one_dir()

    def on_arrow_right_clicked(self, widget, data=None):
        pass

    def _fill_file_liststore(self, path=None):
        model = self.file_liststore
        model.clear()
        self.current_selection = None

        if path:
            self._cur_dir = path  #os.path.realpath(path)

            # Reset scrollbars since display has changed
            self.file_vadj.set_value(0)
            self.file_hadj.set_value(0)

        files = []
        folders = []
        if self._filter in self._filters:
            exts = self._filters[self._filter]
        else:
            exts = ''
        dirs = os.listdir(self._cur_dir)
        for obj in dirs:
            if obj[0] == '.' and not self._show_hidden:
                continue
            path = os.path.join(self._cur_dir, obj)
            if os.path.islink(path):
                path = os.readlink(path)
            if os.path.isdir(path):
                folders.append(obj)
            elif os.path.isfile(path):
                ext = os.path.splitext(obj)[1]
                if '*' in exts and not ext in self._hidden_exts:
                    files.append(obj)
                elif '*{0}'.format(ext) in exts:
                    files.append(obj)

        folders.sort(key=str.lower, reverse=False)
        for fname in folders:
            fpath = os.path.join(self._cur_dir, fname)
            icon = self.icons.get_for_directory(fpath)
            model.append([0, icon, fname, None, None])

        files.sort(key=str.lower, reverse=False)
        for fname in files:
            fpath = os.path.join(self._cur_dir, fname)
            icon = self.icons.get_for_file(fname)
            size, date = self._get_file_data(fpath)
            model.append([0, icon, fname, size, date])

        self._update_nav_buttons()

    def _get_file_data(self, fpath):
        size = os.path.getsize(fpath)
        if size >= 1E9:
            size_str = "{:.1f} GB".format(size / 1E9)
        elif size >= 1E6:
            size_str = "{:.1f} MB".format(size / 1E6)
        elif size >= 1E3:
            size_str = "{:.1f} KB".format(size / 1E3)
        else:
            size_str = "{} bytes".format(size)
        tstamp = os.path.getmtime(fpath)
        date_str = datetime.fromtimestamp(tstamp).strftime("%m/%d/%y %X")
        return size_str, date_str

    def on_select_toggled(self, widget, path):
        model = self.file_liststore
        model[path][0] = not model[path][0]

    def on_filechooser_treeview_cursor_changed(self, widget):
        path = widget.get_cursor()[0]
        # Prevent emiting selection changed on double click
        if path == self.current_selection:
            return
        self.current_selection = path
        fname = self.file_liststore[path][2]
        fpath = os.path.join(self._cur_dir, fname)
        self.emit('selection-changed', fpath)

    def on_filechooser_treeview_row_activated(self, widget, path, colobj):
        fname = self.file_liststore[path][2]
        fpath = os.path.join(self._cur_dir, fname)
        if os.path.isfile(fpath):
            self.emit('file-activated', fpath)
        elif os.path.isdir(fpath):
            self._fill_file_liststore(fpath)

    def on_file_name_editing_started(self, renderer, entry, row):
        self.emit('filename-editing-started', entry)

    def on_file_name_edited(self, widget, row, new_name):
        model = self.file_liststore
        old_name = model[row][2]
        old_path = os.path.join(self._cur_dir, old_name)
        new_path = os.path.join(self._cur_dir, new_name)
        if old_name == new_name:
            return
        if not os.path.exists(new_path):
            msg = "Renamed {} to {}".format(old_name, new_name)
            log.info(msg)
            self.info(msg)
            os.rename(old_path, new_path)
            model[row][2] = new_name
        else:
            msg = "Destination file already exists, won't rename"
            log.warning(msg)
            self.warn(msg)
            # TODO add overwrite confirmation dialog

    def on_file_treeview_button_release_event(self, widget, data=None):
        self.emit('button-release-event')

    # =======================================
    #   Methods to be called externally
    # =======================================

    # Get filechooser object to embed in main window
    def get_filechooser_widget(self):
        return self.builder.get_object('vbox1')

    # Add filter by name and list of extensions to display
    def add_filter(self, name, ext):
        self._filters[name] = ext

    # Delete filter by name
    def remove_filter(self, name):
        if name in self._filters:
            del self._filters[name]
            return True
        return False

    # Set current filter by name
    def set_filter(self, name):
        if name in self._filters:
            self._filter = name
            self._fill_file_liststore()
            return True
        return False

    # Get current filter
    def get_filter(self):
        if self._filter in self._filters:
            return [self._filter, self._filters[self._filter]]
        return None

    # Get names of all specified filters
    def get_filters(self):
        filters = []
        for filter in self._filters:
            filters.append(filter)
        return filters

    # Get whether hidden files are shown
    def get_show_hidden(self):
        return self._show_hidden

    # Set whether hidden files are shown
    def set_show_hidden(self, setting):
        self._show_hidden = setting

    # Get the path of the current display directory
    def get_current_folder(self):
        return self._cur_dir

    # Set current display directory to path
    def set_current_folder(self, fpath):
        if os.path.exists(fpath):
            self._fill_file_liststore(fpath)
            return True
        return False

    # Get absolute path at cursor
    def get_path_at_cursor(self):
        path = self.file_treeview.get_cursor()[0]
        if path is not None:
            fname = self.file_liststore[path][2]
            fpath = os.path.join(self._cur_dir, fname)
            return fpath
        return None

    # Set cursor at path
    def set_cursor_at_path(self, fpath):
        model = self.file_liststore
        tree = self.file_treeview
        if not os.path.exists(fpath):
            return False
        fpath, fname = os.path.split(fpath)
        if fpath != self._cur_dir:
            self._fill_file_liststore(fpath)
        for row in range(len(model)):
            if model[row][2] == fname:
                tree.set_cursor(row)
                return True
        return False

    # Get paths for selected
    def get_selected(self):
        model = self.file_liststore
        paths = []
        for row in range(len(model)):
            if model[row][0] == 1:
                fpath = os.path.join(self._cur_dir, model[row][2])
                paths.append(fpath)
        if len(paths) == 0:
            return None
        return paths

    # Check checkbox at file path
    def set_selected(self, fpath):
        model = self.file_liststore
        tree = self.file_treeview
        if not os.path.exists(fpath):
            return False
        fpath, fname = os.path.split(fpath)
        if fpath != self._cur_dir:
            self._fill_file_liststore(fpath)
        for row in range(len(model)):
            if model[row][2] == fname:
                model[row][0] = 1
                return True
        return False

    # Check all checkboxes in current display directory
    def select_all(self, fpath=None):
        model = self.file_liststore
        if fpath is not None:
            if os.path.isdir(fpath):
                self._fill_file_liststore(fpath)
            else:
                return False
        for row in range(len(model)):
            model[row][0] = 1
        return True

    # Uncheck all checkboxes in current display directory
    def unselect_all(self):
        model = self.file_liststore
        for row in range(len(model)):
            model[row][0] = 0

    # Get paths to current mounts
    def get_mounts(self):
        mounts = self.mounts.get_mounts()
        paths = []
        for mount in mounts:
            path = mount.get_root().get_path()
            paths.append(path)
        return paths

    # Get list of user bookmarks
    def get_bookmarks(self):
        return self.bookmarks.get()

    # Add bookmark
    def add_bookmark(self, path):
        if not path in self.places:
            self.bookmarks.add(path)
            self._update_bookmarks()

    # Remove bookmark
    def remove_bookmark(self, path):
        if not path in self.places:
            self.bookmarks.remove(path)
            self._update_bookmarks()

    # Clear all bookmarks
    def clear_bookmarks(self):
        self.bookmarks.clear()
        self._update_bookmarks()

    # Display parent of current directory
    def up_one_dir(self):
        path = os.path.dirname(self._cur_dir)
        self._fill_file_liststore(path)

    # Cut selected files
    def cut_selected(self):
        files = self.get_selected()
        if files is None:
            log.error("No files selected to cut")
            return False
        self._files = files
        self._copy = False
        log.debug("Files to cut: {}".format(files))
        return True

    # Copy selected files
    def copy_selected(self):
        files = self.get_selected()
        if files is None:
            log.error("No files selected to copy")
            return False
        self._files = files
        self._copy = True
        log.debug("Files to copy: {}".format(files))
        return True

    # Paste previously cut/copied files to current directory
    def paste(self):
        src_list = self._files
        if src_list is None:
            return False
        dst_dir = self._cur_dir
        if self._copy:
            for src in self._files:
                self._copy_file(src, dst_dir)
        elif not self._copy:
            for src in self._files:
                self._move_file(src, dst_dir)
            self._files = None
        self._fill_file_liststore()
        return True

    # Save file as, if path is specified it will be saved in that directory
    def save_as(self, path=None):
        model = self.file_liststore
        tree = self.file_treeview
        if path is None:
            path = self.get_selected()[0]
        if not os.path.exists(path) or path is None:
            return False
        fpath, fname = os.path.split(path)
        new_name = self._copy_file(path, fdir)
        for row in range(len(model)):
            if model[row][2] == new_name:
                break
        focus_column = self.builder.get_object('col_file_name')
        model[row][0] = 1
        tree.set_cursor(row, focus_column, True)

    # Create a new folder in the current directory
    def new_folder(self):
        model = self.file_liststore
        tree = self.file_treeview

        name = "New Folder"
        count = 1
        while os.path.exists(os.path.join(self._cur_dir, name)):
            name = 'New Folder {0}'.format(count)
            count += 1

        path = os.path.join(self._cur_dir, name)
        os.makedirs(path)
        self._fill_file_liststore()

        for row in range(len(model)):
            if model[row][2] == name:
                break
        focus_column = self.builder.get_object('col_file_name')
        model[row][0] = 1
        tree.set_cursor(row, focus_column, True)

    # Move selected files to trash (see file_util code at end)
    def delete_selected(self):
        paths = self.get_selected()
        if paths is None:
            return
        num = len(paths)
        for path in paths:
            info = move2trash(path)
        self._fill_file_liststore()

        # Show ERROR/INFO message at bottom of screen
        self.emit('error', info[0], info[1])

    # =======================================
    #   Drag and Drop TODO
    # =======================================

    def on_file_treeview_drag_begin(self, data, som):
        log.debug("drag {0} {1}".format(data, som))

    def on_file_treeview_drag_data_received(self):
        log.debug("got drag")

    def drag_data_received_cb(self, treeview, context, x, y, selection, info,
                              timestamp):
        drop_info = treeview.get_dest_row_at_pos(x, y)
        log.debug("got drag")
        if drop_info:
            model = treeview.get_model()
            path, position = drop_info
            data = selection.data
            # do something with the data and the model
            log.debug("{0} {1} {2}".format(model, path, data))
        return

    # =======================================
    #   Bookmark treeview
    # =======================================

    def on_mount_added(self, volume, mount):
        path = mount.get_root().get_path()
        name = os.path.split(path)[1]
        msg = 'External storage device "{}" mounted'.format(name)
        log.info(msg)
        self.info(msg)
        self._update_bookmarks()

    def on_mount_removed(self, volume, mount):
        path = mount.get_root().get_path()
        name = os.path.split(path)[1]
        msg = 'External storage device "{}" removed'.format(name)
        log.info(msg)
        self.info(msg)
        if self._cur_dir.startswith(path):
            self.file_liststore.clear()
            self._update_nav_buttons('')
        self._update_bookmarks()

    def on_bookmark_treeview_cursor_changed(self, widget):
        model = self.bookmark_liststore
        path, column = widget.get_cursor()
        fpath = model[path][2]
        if column == self.eject_column and model[path][3] == True:
            os.system('eject "{0}"'.format(fpath))
            if fpath == self._cur_dir:
                self.file_liststore.clear()
            return
        self._fill_file_liststore(fpath)

    def on_add_bookmark_button_release_event(self, widget, data=None):
        path = self.file_treeview.get_cursor()[0]
        if path is None:
            fpath = self._cur_dir
        else:
            fname = self.file_liststore[path][2]
            fpath = os.path.join(self._cur_dir, fname)
        if not os.path.isdir(fpath):
            return
        self.add_bookmark(fpath)

    def on_remove_bookmark_button_release_event(self, widget, data=None):
        path = self.bookmark_treeview.get_cursor()[0]
        if path is None:
            return
        fpath = self.bookmark_liststore[path][2]
        self.remove_bookmark(fpath)

    def _update_bookmarks(self):
        places = sorted(self.places, key=len, reverse=False)
        mounts = sorted(self.get_mounts(), key=self.sort, reverse=False)
        bookmarks = sorted(self.bookmarks.get(), key=self.sort, reverse=False)
        model = self.bookmark_liststore
        model.clear()

        # Add the places
        for path in places:
            name = os.path.split(path)[1]
            icon = self.icons.get_for_directory(path)
            model.append([icon, name, path, False])

        # Add the mounts
        for path in mounts:
            name = os.path.split(path)[1]
            icon = self.icons.get_for_device('USBdrive')
            model.append([icon, name, path, True])

        # Add the seperator
        model.append([None, None, None, False])

        # Add the bookmarks
        for bookmark in bookmarks:
            path, name = bookmark
            if not os.path.exists(path):
                continue
            if name == '':
                name = os.path.split(path)[1]
            icon = self.icons.get_for_directory(path)
            model.append([icon, name, path, False])

    # Generate sort key based on file basename
    def sort(self, path):
        return os.path.basename(path[0]).lower()

    # If name is None row should be a separator
    def bookmark_separator(self, model, iter):
        return self.bookmark_liststore.get_value(iter, 1) is None

    # =======================================
    #   File utilities
    # =======================================

    def _copy_file(self, src, dst_dir):
        src_dir, src_name = os.path.split(src)
        dst_name = src_name

        # find a unique copy name
        if src_dir == dst_dir:
            name, ext = os.path.splitext(dst_name)
            if '_copy' in name:
                name = name.rpartition('_copy')[0]

            count = 1
            while os.path.exists(os.path.join(dst_dir, dst_name)):
                dst_name = '{0}_copy{1}{2}'.format(name, count, ext)
                count += 1

        dst = os.path.join(dst_dir, dst_name)

        if os.path.exists(dst):
            text = "Destination already exists! \n Overwrite {}?".format(
                dst_name)
            overwrite = self.ok_cancel_dialog.run(text)
            if not overwrite:
                msg = "User selected not to overwrite {}".format(dst_name)
                log.info(msg)
                self.info(msg)
                return

        msg = 'Copying "{0}" to "{1}"'.format(src_name, dst_dir)
        log.info(msg)
        self.info(msg)

        if os.path.isfile(src):
            shutil.copy2(src, dst)

        else:
            shutil.copytree(src, dst)

        self._fill_file_liststore()
        return dst_name

    def _move_file(self, src, dst_dir):
        src_dir, src_name = os.path.split(src)

        if src_dir == dst_dir:
            msg = "MOVE ERROR: Source and destination are the same"
            log.error(msg)
            self.error(msg)
            return

        dst = os.path.join(dst_dir, src_name)

        if os.path.exists(dst):
            text = "Destination already exists! \n Overwrite {}?".format(
                src_name)
            overwrite = self.ok_cancel_dialog.run(text)
            if not overwrite:
                msg = 'User selected not to overwrite "{}"'.format(src_name)
                log.info(msg)
                self.info(msg)
                return

        msg = 'Moving "{0}" to "{1}"'.format(src_name, dst_dir)
        log.info(msg)
        self.info(msg)
        shutil.move(src, dst)

    # Shortcut methods to emit errors and print to terminal
    def error(self, text):
        self.emit('error', 'ERROR', text)

    def warn(self, text):
        self.emit('error', 'WARN', text)

    def info(self, text):
        self.emit('error', 'INFO', text)