1 # -*- coding: UTF-8 -*-
2 # This program is free software; you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation; either version 2 of the License, or
5 # (at your option) any later version.
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU Library General Public License for more details.
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16 # See the COPYING file for license information.
18 # Copyright (c) 2006, 2007, 2008 Guillaume Chazarain <guichaz@gmail.com>
24 assert gtk
.pygtk_version
>= (2, 8)
29 from pysize
.ui
.gtk
.gazpacho_loader
.loader
import ObjectBuilder
31 from pysize
.ui
.gtk
.pysize_widget
import PysizeWidget
32 from pysize
.ui
.gtk
.help import show_help
33 from pysize
.ui
.utils
import human_unit
, sanitize_string
34 from pysize
.core
import compute_size
35 from pysize
.core
.compute_size
import size_observable
36 from pysize
.core
import history
37 from pysize
.core
.deletion
import get_deleted
, restore
38 from pysize
.version
import VERSION
40 def hover_changed(main_widget
, hover_node
, status_bar
):
42 nodes
= main_widget
.get_selected_nodes()
47 nodes
= [main_widget
.tree
.root
]
48 size
= sum([node
.size
for node
in nodes
])
49 text
= ' + '.join([node
.get_fullname() for node
in nodes
])
50 label
= human_unit(size
) + ' | ' + sanitize_string(text
, max_length
=1000)
51 status_bar
.push(0, label
)
53 def size_observer(progress_bar
):
55 if now
- size_observer
.last_pulse
> 0.04:
56 size_observer
.last_pulse
= now
57 gobject
.idle_add(lambda: progress_bar
.pulse())
58 size_observer
.last_pulse
= time
.time()
60 def hide_pbar(progress_bar
, visible
):
66 def update_action(zoom_fit_action
, main_widget
):
67 # Enable if the zoom is not already 'auto' and there is a tree
68 enable
= main_widget
.min_size
!= 'auto' and \
69 not not main_widget
.tree
.root
.get_fullpaths()
70 zoom_fit_action
.set_sensitive(enable
)
72 def update_title(window
, main_widget
):
73 root
= main_widget
.tree
.root
74 window
.set_title('Pysize - %s | %s' %
75 (human_unit(root
.size
), sanitize_string(root
.get_name())))
77 def update_back_action(back_action
, next_insertion_index
):
78 back_action
.set_sensitive(next_insertion_index
> 1)
80 def update_forward_action(forward_action
, next_insertion_index
, history
):
81 forward_action
.set_sensitive(next_insertion_index
< len(history
))
83 def update_hist_menu(main_widget
, hist_menu
, next_insertion_index
, hist_list
):
84 class activator(object):
85 def __init__(self
, index
):
88 def activate(self
, unused_menu_item
):
89 main_widget
.set_paths(history
.go_to_history(self
.index
))
91 for nr
, item
in enumerate(hist_menu
):
92 if nr
> 2: # Keep 'Back' and 'Forward'
93 hist_menu
.remove(item
)
94 item
= gtk
.SeparatorMenuItem()
97 for nr
, (paths
, name
) in enumerate(hist_list
):
98 # We want underscores, not mnemonics
99 name
= sanitize_string(name
, max_length
=100).replace('_', '__')
100 item
= gtk
.CheckMenuItem(name
)
101 if nr
+ 1 == next_insertion_index
:
102 item
.set_active(True)
103 item
.connect('activate', activator(nr
).activate
)
104 item
.set_draw_as_radio(True)
108 def show_deleted_files(dialog
, treeview
):
109 model
= treeview
.get_model()
111 for path
in get_deleted():
112 size
= human_unit(compute_size
.slow(path
, account_deletion
=False))
113 model
.append((size
, path
))
117 def iter_selection(treeview
):
118 model
= treeview
.get_model()
119 selection
= treeview
.get_selection().get_selected_rows()[1]
122 for treepath
in selection
:
123 treeiter
= model
.get_iter(treepath
)
124 path
= model
.get_value(treeiter
, 1)
127 def deleted_files_update_selection(treeview
, restore_button
, delete_button
,
129 selected_paths
= [s
[1] for s
in iter_selection(treeview
)]
130 sensitive
= len(selected_paths
) > 0
131 restore_button
.set_sensitive(sensitive
)
132 delete_button
.set_sensitive(sensitive
)
133 selected_paths
= selected_paths
or get_deleted()
134 total
= compute_size
.slow_sum(selected_paths
, account_deletion
=False)
135 deleted_files_size
.set_text(human_unit(total
))
137 def restore_deleted_files(treeview
, main_widget
):
138 if treeview
.get_selection().count_selected_rows() > 0:
139 model
= treeview
.get_model()
140 for treeiter
, path
in iter_selection(treeview
):
142 model
.remove(treeiter
)
143 main_widget
.schedule_new_tree()
145 def delete_deleted_files(treeview
, main_widget
):
146 if treeview
.get_selection().count_selected_rows() <= 0:
148 dialog
= gtk
.MessageDialog(parent
=None, flags
=gtk
.DIALOG_MODAL
,
149 type=gtk
.MESSAGE_WARNING
,
150 buttons
=gtk
.BUTTONS_YES_NO
,
151 message_format
='The selected will be permanently'
152 ' deleted, do you really want to delete them?')
153 response
= dialog
.run()
155 if response
== gtk
.RESPONSE_YES
:
156 model
= treeview
.get_model()
157 for treeiter
, path
in iter_selection(treeview
):
160 if os
.path
.isdir(path
):
161 def rmtree_error(function
, path
, excinfo
):
163 shutil
.rmtree(path
, onerror
=rmtree_error
)
168 model
.remove(treeiter
)
169 main_widget
.schedule_new_tree()
172 about
= gtk
.AboutDialog()
173 about
.set_name('Pysize')
174 about
.set_version(VERSION
)
175 about
.set_copyright('Copyright © 2006-2007 Guillaume Chazarain')
176 about
.set_website('http://guichaz.free.fr/pysize')
177 logo_filename
= os
.path
.join(os
.path
.dirname(__file__
), 'logo.svg')
179 logo
= gtk
.gdk
.pixbuf_new_from_file(logo_filename
)
180 logo
= logo
.scale_simple(200, 200, gtk
.gdk
.INTERP_HYPER
)
183 # Not showing the logo is harmless
190 dialog
= gtk
.MessageDialog(parent
=None, flags
=gtk
.DIALOG_MODAL
,
191 type=gtk
.MESSAGE_WARNING
,
192 buttons
=gtk
.BUTTONS_YES_NO
,
193 message_format
='Some files have been '
194 'scheduled for deletion, but have not been '
195 'deleted. Do you still want to quit?')
196 response
= dialog
.run()
198 if response
!= gtk
.RESPONSE_YES
:
204 class PysizeWindow(object):
205 def __init__(self
, options
, args
):
206 create_widget
= lambda **unused_kwargs
: PysizeWidget(options
, args
)
207 glade_filename
= os
.path
.join(os
.path
.dirname(__file__
),
209 builder
= ObjectBuilder(glade_filename
,
210 custom
= {'pysize_widget': create_widget
})
212 main_widget
= builder
.get_widget('main_widget')
213 status_bar
= builder
.get_widget('status_bar')
214 main_widget
.connect('hover_changed', hover_changed
, status_bar
)
216 progress_bar
= builder
.get_widget('progress_bar')
217 size_observable
.add_observer(lambda: size_observer(progress_bar
))
218 pbar_hider
= lambda w
, building
: hide_pbar(progress_bar
, building
)
219 main_widget
.connect('building-tree-state-changed', pbar_hider
)
221 max_depth
= builder
.get_widget('max_depth')
222 spin_init
= lambda w
: max_depth
.set_value(main_widget
.options
.max_depth
)
223 main_widget
.connect('realize', spin_init
)
225 ui_manager
= builder
.get_widget('uimanager')
226 zoom_fit_action
= ui_manager
.get_action('ui/toolbar/ZoomFit')
227 action_updater
= lambda w
, building
: update_action(zoom_fit_action
, w
)
228 main_widget
.connect('building-tree-state-changed', action_updater
)
230 # Disable actions when the tree is empty: pysize launched with no arg
231 class disabler(object):
232 def __init__(self
, action
):
234 def disable(self
, main_widget
, building
):
235 sensitive
= not not main_widget
.tree
.root
.get_fullpaths()
236 self
.action
.set_sensitive(sensitive
)
237 for path
in 'ui/toolbar/ParentDirectory', \
238 'ui/menubar/ActionsMenu/QuickRefresh', \
239 'ui/menubar/ActionsMenu/CompleteReload', \
240 'ui/toolbar/ZoomIn', 'ui/toolbar/ZoomOut':
241 action
= ui_manager
.get_action(path
)
242 action_disabler
= disabler(action
).disable
243 main_widget
.connect('building-tree-state-changed', action_disabler
)
245 back_action
= ui_manager
.get_action('ui/toolbar/Back')
246 back_action
.set_sensitive(False)
247 update_back
= lambda n
, h
: update_back_action(back_action
, n
)
248 history
.history_observable
.add_observer(update_back
)
250 forward_action
= ui_manager
.get_action('ui/toolbar/Forward')
251 forward_action
.set_sensitive(False)
252 update_forw
= lambda n
, h
: update_forward_action(forward_action
, n
, h
)
253 history
.history_observable
.add_observer(update_forw
)
255 hist_menu_action
= ui_manager
.get_action('ui/menubar/HistoryMenu')
256 hist_menu
= hist_menu_action
.get_proxies()[0].get_submenu()
257 upd_hist
= lambda n
, h
: update_hist_menu(main_widget
, hist_menu
, n
, h
)
258 history
.history_observable
.add_observer(upd_hist
)
260 toolbar
= builder
.get_widget('toolbar')
261 toolbar_remaining
= builder
.get_widget('toolbar_remaining')
262 toolbar_remaining
.unparent()
263 tool_item
= gtk
.ToolItem()
264 tool_item
.add(toolbar_remaining
)
265 toolbar
.insert(tool_item
, -1)
267 self
.add_refresh_buttons(toolbar
, ui_manager
)
269 deleted_files_dialog
= builder
.get_widget('deleted_files_dialog')
270 deleted_files_treeview
= builder
.get_widget('deleted_files_treeview')
271 deleted_files_restore
= builder
.get_widget('deleted_files_restore')
272 deleted_files_delete
= builder
.get_widget('deleted_files_delete')
273 deleted_files_size
= builder
.get_widget('deleted_files_size')
274 deleted_files_cb
= lambda: deleted_files_update_selection(
275 deleted_files_treeview
, deleted_files_restore
, deleted_files_delete
,
277 deleted_files_event
= lambda *args
: gobject
.idle_add(deleted_files_cb
)
278 deleted_files_treeview
.get_selection().set_mode(gtk
.SELECTION_MULTIPLE
)
280 logo_filename
= os
.path
.join(os
.path
.dirname(__file__
), 'logo.svg')
282 gtk
.window_set_default_icon_from_file(logo_filename
)
284 # Not showing the logo is harmless
288 'quit_action': lambda *args
: quit_gui(),
289 'quick_refresh_action': lambda w
: main_widget
.schedule_new_tree(),
290 'complete_reload_action': lambda w
: main_widget
.complete_reload(),
291 'zoom_fit_action': lambda w
: main_widget
.zoom_fit(),
292 'zoom_in_action': lambda w
: main_widget
.zoom_in(),
293 'zoom_out_action': lambda w
: main_widget
.zoom_out(),
294 'parent_directory_action': lambda w
: main_widget
.parent_directory(),
295 'open_action': lambda w
: main_widget
.open(),
296 'back_action': lambda w
: main_widget
.set_paths(history
.back()),
297 'forw_action': lambda w
: main_widget
.set_paths(history
.forward()),
298 'deleted_files_action': lambda w
:
299 show_deleted_files(deleted_files_dialog
, deleted_files_treeview
),
300 'close_deleted_files': lambda b
: deleted_files_dialog
.hide(),
301 'restore_deleted_files': lambda b
:
302 restore_deleted_files(deleted_files_treeview
, main_widget
),
303 'delete_deleted_files': lambda b
:
304 delete_deleted_files(deleted_files_treeview
, main_widget
),
305 'max_depth_changed': main_widget
.max_depth_changed
,
306 'about_action': lambda a
: show_about(),
307 'help_action': lambda a
: show_help(),
308 'deleted_files_change_selection': deleted_files_event
310 builder
.signal_autoconnect(callbacks
)
312 window
= builder
.get_widget('main_window')
313 title_updater
= lambda w
, tree
: update_title(window
, main_widget
)
314 main_widget
.connect('building-tree-state-changed', title_updater
)
317 def add_refresh_buttons(self
, toolbar
, ui_manager
):
318 quick
= gtk
.MenuToolButton(stock_id
='')
319 tooltips
= gtk
.Tooltips()
320 quick_act
= ui_manager
.get_action('ui/menubar/ActionsMenu/QuickRefresh')
321 quick_act
.connect_proxy(quick
)
322 quick
.set_tooltip(tooltips
, quick_act
.get_property('tooltip'))
325 complete
= gtk
.MenuItem()
326 cmp_act
= ui_manager
.get_action('ui/menubar/ActionsMenu/CompleteReload')
327 cmp_act
.connect_proxy(complete
)
328 tooltips
.set_tip(complete
, cmp_act
.get_property('tooltip'))
330 toolbar
.insert(quick
, 1)
331 # The gtk.Tooltips object need to be kept referenced, otherwise it will
332 # be garbage collected and will destroy its associated tooltips.
333 PysizeWindow
.__tooltips
__ = tooltips