iconview.py
1 #!/usr/bin/env python 2 3 ''' 4 This IconView widget shows the contents of the currently selected 5 directory on the disk. 6 7 it is based on a tutorial from ZetCode PyGTK tutorial 8 the original code is under the BSD license 9 author: jan bodnar 10 website: zetcode.com 11 12 Copyright 2012 Norbert Schechner 13 nieson@web.de 14 15 This program is free software; you can redistribute it and/or modify 16 it under the terms of the GNU General Public License as published by 17 the Free Software Foundation; either version 2 of the License, or 18 (at your option) any later version. 19 20 This program is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 GNU General Public License for more details. 24 25 You should have received a copy of the GNU General Public License 26 along with this program; if not, write to the Free Software 27 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 28 29 ''' 30 31 import gtk 32 import gobject 33 import os 34 import mimetypes 35 import gio 36 37 # constants 38 _ASCENDING = 0 39 _DESCENDING = 1 40 _FOLDERFIRST = 2 41 _FILEFIRST = 3 42 COL_PATH = 0 43 COL_PIXBUF = 1 44 COL_IS_DIRECTORY = 2 45 46 # prepared for localization 47 import gettext 48 _ = gettext.gettext 49 50 # icon_size must be an integer, inbetween 12 and 96, default is 48 51 # start_dir is the start directory, given as a string like "~/linuxcnc-dev/configs/sim/gmoccapy/nc_files" 52 # user_dir is a user defined directory to jump to, given as a string like "/media" 53 # filetypes is a comma separated string, giving the extensions of the files to be shown in the widget 54 # like "ngc,py,png,hal" 55 # sortorder one of ASCENDING, DESCENDING, FOLDERFIRST, FILEFIRST 56 class IconFileSelection(gtk.HBox): 57 58 # ToDo: 59 # - make the button up and down work to move faster from top to bottom 60 # unfortuantely the selection of column is not availible in pygtk before 2.22 61 62 __gtype_name__ = 'IconFileSelection' 63 __gproperties__ = { 64 'icon_size' : (gobject.TYPE_INT, 'Icon Size', 'Sets the size of the displayed icon', 65 12, 96, 48, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 66 'start_dir' : (gobject.TYPE_STRING, 'start directory', 'Sets the directory to start in', 67 "/", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 68 'jump_to_dir' : (gobject.TYPE_STRING, 'jump to directory', 'Sets the directory to jump to ', 69 "~", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 70 'filetypes' : (gobject.TYPE_STRING, 'file filter', 'Sets the filter for the file types to be shown', 71 "ngc,py", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 72 'sortorder' : (gobject.TYPE_INT, 'sorting order', '0 = ASCENDING, 1 = DESCENDING", 2 = FOLDERFIRST, 3 = FILEFIRST', 73 0, 3, 2, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT), 74 } 75 __gproperties = __gproperties__ 76 77 __gsignals__ = { 78 'selected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), 79 'sensitive': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_BOOLEAN,)), 80 'exit': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()), 81 } 82 83 def __init__(self): 84 super(IconFileSelection, self).__init__() 85 86 # set some default values' 87 self.icon_size = 48 88 self.start_dir = os.path.expanduser('/') 89 self.cur_dir = self.start_dir 90 self.user_dir = os.path.expanduser('~') 91 self.jump_to_dir = os.path.expanduser('/tmp') 92 self.filetypes = ("ngc,py") 93 self.sortorder = _FOLDERFIRST 94 # This will hold the path we will return 95 self.path = "" 96 self.button_state = {} 97 self.old_button_state = {} 98 99 # Make the GUI and connect signals 100 vbox = gtk.VBox(False, 0) 101 102 self.buttonbox = gtk.HButtonBox() 103 self.buttonbox.set_layout(gtk.BUTTONBOX_EDGE) 104 self.buttonbox.set_property("homogeneous", True) 105 vbox.pack_end(self.buttonbox, False, False, 0) 106 107 self.btn_home = gtk.Button() 108 self.btn_home.set_size_request(56, 56) 109 image = gtk.Image() 110 image.set_from_stock(gtk.STOCK_HOME, gtk.ICON_SIZE_LARGE_TOOLBAR) 111 self.btn_home.set_image(image) 112 self.btn_home.set_tooltip_text(_("Move to your home directory")) 113 self.buttonbox.add(self.btn_home) 114 115 self.btn_dir_up = gtk.Button(); 116 self.btn_dir_up.set_size_request(56, 56) 117 image = gtk.Image() 118 image.set_from_stock(gtk.STOCK_GOTO_TOP, gtk.ICON_SIZE_LARGE_TOOLBAR) 119 self.btn_dir_up.set_image(image) 120 self.btn_dir_up.set_tooltip_text(_("Move to parent directory")) 121 self.buttonbox.add(self.btn_dir_up) 122 123 self.btn_sel_prev = gtk.Button() 124 self.btn_sel_prev.set_size_request(56, 56) 125 image = gtk.Image() 126 image.set_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_LARGE_TOOLBAR) 127 self.btn_sel_prev.set_image(image) 128 self.btn_sel_prev.set_tooltip_text(_("Select the previous file")) 129 self.buttonbox.add(self.btn_sel_prev) 130 131 self.btn_sel_next = gtk.Button() 132 self.btn_sel_next.set_size_request(56, 56) 133 image = gtk.Image() 134 image.set_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_LARGE_TOOLBAR) 135 self.btn_sel_next.set_image(image) 136 self.btn_sel_next.set_tooltip_text(_("Select the next file")) 137 self.buttonbox.add(self.btn_sel_next) 138 139 # ToDo : Find out how to move one line down or up 140 # self.btn_go_down = gtk.Button() 141 # self.btn_go_down.set_size_request(56,56) 142 # image = gtk.Image() 143 # image.set_from_stock(gtk.STOCK_GO_DOWN,48) 144 # self.btn_go_down.set_image(image) 145 # self.buttonbox.add(self.btn_go_down) 146 # 147 # self.btn_go_up = gtk.Button() 148 # self.btn_go_up.set_size_request(56,56) 149 # image = gtk.Image() 150 # image.set_from_stock(gtk.STOCK_GO_UP,48) 151 # self.btn_go_up.set_image(image) 152 # self.buttonbox.add(self.btn_go_up) 153 # ToDo : End 154 155 self.btn_jump_to = gtk.Button() 156 self.btn_jump_to.set_size_request(56, 56) 157 image = gtk.Image() 158 image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_LARGE_TOOLBAR) 159 self.btn_jump_to.set_image(image) 160 self.btn_jump_to.set_tooltip_text(_("Jump to user defined directory")) 161 self.buttonbox.add(self.btn_jump_to) 162 163 self.btn_select = gtk.Button() 164 self.btn_select.set_size_request(56, 56) 165 image = gtk.Image() 166 image.set_from_stock(gtk.STOCK_OK, gtk.ICON_SIZE_LARGE_TOOLBAR) 167 self.btn_select.set_image(image) 168 self.btn_select.set_tooltip_text(_("select the highlighted file and return the path")) 169 self.buttonbox.add(self.btn_select) 170 171 self.btn_exit = gtk.Button() 172 self.btn_exit.set_size_request(56, 56) 173 image = gtk.Image() 174 image.set_from_stock(gtk.STOCK_STOP, gtk.ICON_SIZE_LARGE_TOOLBAR) 175 self.btn_exit.set_image(image) 176 self.btn_exit.set_tooltip_text(_("Close without returning a file path")) 177 self.buttonbox.add(self.btn_exit) 178 179 self.dirIcon = self._get_icon("folder") 180 181 sw = gtk.ScrolledWindow() 182 sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) 183 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 184 vbox.pack_start(sw, True, True, 0) 185 186 self.file_label = gtk.Label("File Label") 187 vbox.pack_start(self.file_label, False, True, 0) 188 189 self.store = self._create_store() 190 191 self.iconView = gtk.IconView(self.store) 192 self.iconView.set_selection_mode(gtk.SELECTION_SINGLE) 193 194 self.iconView.set_text_column(COL_PATH) 195 self.iconView.set_pixbuf_column(COL_PIXBUF) 196 197 sw.add(self.iconView) 198 self.iconView.grab_focus() 199 self.model = self.iconView.get_model() 200 201 self.btn_dir_up.connect("clicked", self.on_btn_dir_up_clicked) 202 self.btn_home.connect("clicked", self.on_btn_home_clicked) 203 self.btn_sel_next.connect("clicked", self.on_btn_sel_next_clicked) 204 self.btn_sel_prev.connect("clicked", self.on_btn_sel_prev_clicked) 205 # self.btn_go_down.connect("clicked", self.on_btn_go_down_clicked) 206 # self.btn_go_up.connect("clicked", self.on_btn_go_up_clicked) 207 self.btn_jump_to.connect("clicked", self.on_btn_jump_to_clicked) 208 self.btn_select.connect("clicked", self.on_btn_select_clicked) 209 self.btn_exit.connect("clicked", self.on_btn_exit_clicked) 210 # will be emitted, when a icon has been activated, so we keep track of the path 211 self.iconView.connect("item-activated", self._on_item_activated) 212 # will be emitted, when a icon is activated and the ENTER key has been pressed 213 self.iconView.connect("activate-cursor-item", self._on_activate_cursor_item) 214 # will be emmited if the selection has changed, this happens also if the user clicks ones on an icon 215 self.iconView.connect("selection-changed", self._on_selection_changed) 216 # will be emitted, when the widget is destroyed 217 self.connect("destroy", gtk.main_quit) 218 219 self.add(vbox) 220 self.show_all() 221 222 # To use the the events, we have to unmask them 223 self.iconView.add_events(gtk.gdk.BUTTON_PRESS_MASK) 224 self.iconView.connect("button_press_event", self._button_press) 225 226 self._fill_store() 227 self._init_button_state() 228 229 def _init_button_state(self): 230 # we need this to check for differnces in the button state 231 self.button_state["btn_home"] = self.btn_home.get_sensitive() 232 self.button_state["btn_dir_up"] = self.btn_dir_up.get_sensitive() 233 self.button_state["btn_sel_prev"] = self.btn_sel_prev.get_sensitive() 234 self.button_state["btn_sel_next"] = self.btn_sel_next.get_sensitive() 235 self.button_state["btn_jump_to"] = self.btn_jump_to.get_sensitive() 236 self.button_state["btn_select"] = self.btn_select.get_sensitive() 237 self.old_button_state = self.button_state.copy() 238 239 # With the left mouse button and a dobble click, the file can be selected 240 def _button_press(self, widget, event): 241 # left button used? 242 if event.button == 1: 243 # dobble click? 244 if event.type == gtk.gdk._2BUTTON_PRESS: 245 self.btn_select.emit("clicked") 246 247 def _on_activate_cursor_item(self, widget): 248 self.btn_select.emit("clicked") 249 250 def _get_icon(self, name): 251 theme = gtk.icon_theme_get_default() 252 if name == "folder": 253 name = gtk.STOCK_DIRECTORY 254 else: 255 mime = gio.content_type_guess(name) 256 if mime: 257 iconname = gio.content_type_get_icon(mime) 258 icon = theme.choose_icon(iconname.get_names(), self.icon_size, 0) 259 if icon: 260 return gtk.IconInfo.load_icon(icon) 261 else: 262 name = gtk.STOCK_FILE 263 else: 264 name = gtk.STOCK_FILE 265 return theme.load_icon(name, self.icon_size, 0) 266 267 def _create_store(self): 268 store = gtk.ListStore(str, gtk.gdk.Pixbuf, bool) 269 return store 270 271 def _fill_store(self): 272 if self.cur_dir == None: 273 return 274 275 try: 276 self.store.clear() 277 number = 0 278 dirs = [] 279 files = [] 280 for fl in os.listdir(self.cur_dir): 281 # we don't want to add hidden files 282 if fl[0] == '.': 283 continue 284 if os.path.isdir(os.path.join(self.cur_dir, fl)): 285 try: 286 os.listdir(os.path.join(self.cur_dir, fl)) 287 dirs.append(fl) 288 number += 1 289 except OSError: 290 #print ("no rights for ", os.path.join(self.cur_dir, fl), " skip that dir") 291 continue 292 else: 293 try: 294 name, ext = fl.rsplit(".", 1) 295 if "*" in self.filetypes: 296 files.append(fl) 297 number += 1 298 elif ext in self.filetypes: 299 files.append(fl) 300 number += 1 301 except: 302 pass 303 304 if self.sortorder not in [_ASCENDING, _DESCENDING, _FOLDERFIRST, _FILEFIRST]: 305 self.sortorder = _FOLDERFIRST 306 307 if self.sortorder == _ASCENDING or self.sortorder == _DESCENDING: 308 allobjects = dirs 309 allobjects.extend(files) 310 allobjects.sort(cmp = None, key = None, reverse = not self.sortorder == _ASCENDING) 311 312 for obj in allobjects: 313 if os.path.isdir(os.path.join(self.cur_dir, obj)): 314 self.store.append([obj, self.dirIcon, True]) 315 else: 316 icon = self._get_icon(obj) 317 self.store.append([obj, icon, False]) 318 319 dirs.sort(cmp = None, key = None, reverse = False) 320 files.sort(cmp = None, key = None, reverse = False) 321 if self.sortorder == _FOLDERFIRST: 322 for dir in dirs: 323 self.store.append([dir, self.dirIcon, True]) 324 for file in files: 325 icon = self._get_icon(file) 326 self.store.append([file, icon, False]) 327 elif self.sortorder == _FILEFIRST: 328 for file in files: 329 icon = self._get_icon(file) 330 self.store.append([file, icon, False]) 331 for dir in dirs: 332 self.store.append([dir, self.dirIcon, True]) 333 except: 334 pass 335 finally: 336 # check the stat of the button and set them as they should be 337 self.check_button_state() 338 339 def check_button_state(self): 340 if self.model.get_iter_first() == None: 341 state = False 342 else: 343 state = True 344 self.btn_sel_next.set_sensitive(state) 345 self.button_state["btn_sel_next"] = state 346 self.btn_sel_prev.set_sensitive(state) 347 self.button_state["btn_sel_prev"] = state 348 self.btn_select.set_sensitive(state) 349 self.button_state["btn_select"] = state 350 state = self.cur_dir == self.user_dir 351 self.btn_home.set_sensitive(not state) 352 self.button_state["btn_home"] = not state 353 state = self.cur_dir == "/" 354 self.btn_dir_up.set_sensitive(not state) 355 self.button_state["btn_dir_up"] = not state 356 state = self.cur_dir == self.jump_to_dir 357 self.btn_jump_to.set_sensitive(not state) 358 self.button_state["btn_jump_to"] = not state 359 state = self.iconView.get_cursor() == None 360 self.btn_select.set_sensitive(not state) 361 self.button_state["btn_select"] = not state 362 self.state_changed() 363 364 def state_changed(self): 365 # find the differnce 366 diff = set(self.button_state.iteritems()) - set(self.old_button_state.iteritems()) 367 for key in self.button_state.keys(): 368 try: 369 if self.button_state[key] != self.old_button_state[key]: 370 self.emit("sensitive",key, self.button_state[key]) 371 except: 372 continue 373 374 self.old_button_state = self.button_state.copy() 375 376 def show_buttonbox(self, state): 377 if state: 378 self.buttonbox.show() 379 else: 380 self.buttonbox.hide() 381 382 def show_filelabel(self, state): 383 if state: 384 self.file_label.show() 385 else: 386 self.file_label.hide() 387 388 def on_btn_home_clicked(self, widget): 389 self.cur_dir = self.user_dir 390 self._fill_store() 391 392 def on_btn_jump_to_clicked(self, widget): 393 self.cur_dir = self.jump_to_dir 394 self._fill_store() 395 396 def get_iter_last(self, model): 397 itr = model.get_iter_first() 398 last = None 399 while itr: 400 last = itr 401 itr = model.iter_next(itr) 402 return last 403 404 def on_btn_sel_prev_clicked(self, data): 405 if not self.btn_sel_prev.is_sensitive(): 406 return None 407 try: 408 actual = self.iconView.get_cursor()[0] 409 iter = self.model.get_iter(actual) 410 pos = int(self.model.get_string_from_iter(iter)) 411 first = int(self.model.get_string_from_iter(self.model.get_iter_first())) 412 pos = pos - 1 413 if pos < first: 414 pos = int(self.model.get_string_from_iter(self.get_iter_last(self.model))) 415 new_iter = self.model.get_iter_from_string(str(pos)) 416 new_path = self.model.get_path(new_iter) 417 except: 418 new_iter = self.get_iter_last(self.model) 419 new_path = self.model.get_path(new_iter) 420 self.iconView.set_cursor(new_path) 421 self.iconView.select_path(new_path) 422 self.check_button_state() 423 424 def on_btn_sel_next_clicked(self, data): 425 if not self.btn_sel_next.is_sensitive(): 426 return None 427 try: 428 actual = self.iconView.get_cursor()[0] 429 iter = self.model.get_iter(actual) 430 try: 431 new_iter = self.model.iter_next(iter) 432 new_path = self.model.get_path(new_iter) 433 self.iconView.set_cursor(new_path) 434 self.iconView.select_path(new_path) 435 except: 436 first = int(self.model.get_string_from_iter(self.model.get_iter_first())) 437 new_path = self.model.get_path(first) 438 self.iconView.set_cursor(new_path) 439 self.iconView.select_path(new_path) 440 except: 441 self.iconView.set_cursor(0) 442 self.iconView.select_path(0) 443 self.check_button_state() 444 445 # ToDo: find out how to move a line down or up 446 # def on_btn_go_down_clicked(self,data): 447 # print("This is the number of the icon selected", self.iconView.get_cursor()) 448 # col = self.iconView.get_item_column(self.iconView.get_cursor()[0]) 449 # row = self.iconView.get_item_row(self.iconView.get_cursor()[0]) 450 # print("This is the column :", col) 451 # print("This is the row :", row) 452 # print(self.iconView.get_columns()) 453 454 455 # self.iconView.item_activated(self.iconView.get_cursor()[0]) 456 # print("go down") 457 # print("columns = ",self.iconView.get_columns()) 458 # print("column spacing = ", self.iconView.get_column_spacing()) 459 # print("item with = ", self.iconView.get_item_width()) 460 # print("margin = ", self.iconView.get_margin()) 461 # print("visible range = ", self.iconView.get_visible_range()) 462 # print("get cursor = ", self.iconView.get_cursor()) 463 # #print("item row = ", self.iconView.get_item_at_row(self.get_selected())) 464 # print("item column = ", self.iconView.get_item_column(self.get_selected())) 465 # 466 # 467 # def on_btn_go_up_clicked(self,data): 468 # print("go up") 469 # ToDo: end 470 471 def set_icon_size(self, iconsize): 472 try: 473 self.icon_size = iconsize 474 self.dirIcon = self._get_icon("folder") 475 self._fill_store() 476 except: 477 pass 478 479 def set_directory(self, directory): 480 self.cur_dir = os.path.expanduser(directory) 481 self._fill_store() 482 483 def set_filetypes(self, filetypes): 484 self.filetypes = filetypes.split(",") 485 self._fill_store() 486 487 def refresh_filelist(self): 488 self._fill_store() 489 490 def get_selected(self): 491 return self.on_btn_select_clicked(self) 492 493 def on_btn_select_clicked(self, data): 494 try: 495 self.iconView.item_activated(self.iconView.get_cursor()[0]) 496 if self.path: 497 filepath = self.cur_dir + os.path.sep + self.path 498 self.file_label.set_text(filepath) 499 else: 500 self.file_label.set_text("") 501 filepath = None 502 self.emit('selected', filepath) 503 except: 504 pass 505 506 def on_btn_exit_clicked(self, data): 507 if __name__ == "__main__": 508 gtk.main_quit() 509 self.emit('exit') 510 511 def _on_item_activated(self, widget, item): 512 path = self.model[item][COL_PATH] 513 isDir = self.model[item][COL_IS_DIRECTORY] 514 515 if not isDir: 516 self.path = path 517 return 518 519 self.cur_dir = self.cur_dir + os.path.sep + path 520 self._fill_store() 521 # This is only to advise, that the selection wasn't a file, but an directory 522 self.path = None 523 524 def _on_selection_changed(self, widget): 525 if not self.btn_select.get_sensitive(): 526 self.btn_select.set_sensitive(True) 527 self.check_button_state() 528 529 def on_btn_dir_up_clicked(self, widget): 530 self.cur_dir = os.path.dirname(self.cur_dir) 531 self._fill_store() 532 533 def do_get_property(self, property): 534 name = property.name.replace('-', '_') 535 if name in self.__gproperties.keys(): 536 return getattr(self, name) 537 else: 538 raise AttributeError('unknown iconview get_property %s' % property.name) 539 540 def do_set_property(self, property, value): 541 try: 542 name = property.name.replace('-', '_') 543 if name in self.__gproperties.keys(): 544 setattr(self, name, value) 545 self.queue_draw() 546 if name == 'icon_size': 547 self.set_icon_size(value) 548 if name == 'start_dir': 549 self.start_dir = os.path.expanduser(value) 550 self.set_directory(self.start_dir) 551 if name == 'jump_to_dir': 552 self.jump_to_dir = os.path.expanduser(value) 553 self.on_btn_jump_to() 554 if name == 'filetypes': 555 self.set_filetypes(value) 556 if name == 'sortorder': 557 if value not in [_ASCENDING, _DESCENDING, _FOLDERFIRST, _FILEFIRST]: 558 raise AttributeError('unknown property of sortorder %s' % value) 559 else: 560 self.sortorder = value 561 self._fill_store() 562 else: 563 raise AttributeError('unknown iconview set_property %s' % property.name) 564 except: 565 pass 566 567 # for testing without glade editor: 568 def main(): 569 window = gtk.Window(gtk.WINDOW_TOPLEVEL) 570 571 IFS = IconFileSelection() 572 IFS.set_property("filetypes", "*") 573 IFS.set_property("jump_to_dir", "/tmp") 574 575 window.add(IFS) 576 window.connect("destroy", gtk.main_quit) 577 window.show_all() 578 window.set_size_request(680, 480) 579 gtk.main() 580 581 582 if __name__ == "__main__": 583 main()