-
Notifications
You must be signed in to change notification settings - Fork 0
/
BookmarksBar.py
532 lines (449 loc) · 16.9 KB
/
BookmarksBar.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# -*- coding: utf-8 -*-
# Copyright 2015-2016 Pavel_M <plprgt@gmail.com>,
# released under the GNU GPL version 3.
# This plugin is for Zim program by Jaap Karssenberg <jaap.karssenberg@gmail.com>.
#
# This plugin uses an icon from Tango Desktop Project (http://tango.freedesktop.org/)
# (the Tango base icon theme is released to the Public Domain).
import gobject
import gtk
import pango
from zim.actions import toggle_action, action
from zim.plugins import PluginClass, extends, WindowExtension
from zim.notebook import Path
from zim.gui.widgets import TOP, TOP_PANE
from zim.signals import ConnectorMixin
from zim.gui.pathbar import ScrolledHBox
from zim.gui.clipboard import Clipboard
import logging
logger = logging.getLogger('zim.plugins.bookmarksbar')
# Keyboard shortcut constants.
BM_TOGGLE_BAR_KEY ='F4'
BM_ADD_BOOKMARK_KEY ='<alt>1'
class BookmarksBarPlugin(PluginClass):
plugin_info = {
'name': _('BookmarksBar'), # T: plugin name
'description': _('''\
This plugin provides bar for bookmarks.
'''), # T: plugin description
'author': 'Pavel_M',
'help': 'Plugins:BookmarksBar',}
plugin_preferences = (
# key, type, label, default
('max_bookmarks', 'int', _('Maximum number of bookmarks'), 15, (5, 20)), # T: plugin preference
('save', 'bool', _('Save bookmarks'), True), # T: preferences option
('add_bookmarks_to_beginning', 'bool', _('Add new bookmarks to the beginning of the bar'), False), # T: preferences option
)
@extends('MainWindow')
class MainWindowExtension(WindowExtension):
uimanager_xml = '''
<ui>
<menubar name='menubar'>
<menu action='view_menu'>
<placeholder name='plugin_items'>
<menuitem action='toggle_show_bookmarks'/>
</placeholder>
</menu>
<menu action='tools_menu'>
<placeholder name='plugin_items'>
<menuitem action='add_bookmark'/>
</placeholder>
</menu>
</menubar>
<toolbar name='toolbar'>
<placeholder name='tools'>
<toolitem action='toggle_show_bookmarks'/>
</placeholder>
</toolbar>
</ui>'''
def __init__(self, plugin, window):
WindowExtension.__init__(self, plugin, window)
self.widget = BookmarkBar(self.window.ui, self.uistate,
self.window.pageview.get_page)
self.widget.show_all()
# Add a new option to the Index popup menu.
try:
self.widget.connectto(self.window.pageindex.treeview,
'populate-popup', self.on_populate_popup)
except AttributeError:
logger.error('BookmarksBar: popup menu not initialized.')
# Show/hide bookmarks.
self.uistate.setdefault('show_bar', True)
self.toggle_show_bookmarks(self.uistate['show_bar'])
# Init preferences in self.widget.
self.widget.on_preferences_changed(plugin.preferences)
self.widget.connectto(plugin.preferences, 'changed',
lambda o: self.widget.on_preferences_changed(plugin.preferences))
def teardown(self):
if self.widget:
try:
self.window.remove(self.widget)
except ValueError:
pass
self.widget.disconnect_all()
self.widget = None
def hide_widget(self):
'''Hide Bar.'''
self.window.remove(self.widget)
def show_widget(self):
'''Show Bar.'''
self.window.add_widget(self.widget, (TOP_PANE, TOP))
def on_populate_popup(self, treeview, menu):
'''Add 'Add Bookmark' option to the Index popup menu.'''
path = treeview.get_selected_path()
if path:
item = gtk.SeparatorMenuItem()
menu.prepend(item)
item = gtk.MenuItem(_('Add Bookmark')) # T: menu item bookmark plugin
page = self.window.ui.notebook.get_page(path)
item.connect('activate', lambda o: self.widget.add_new_page(page))
menu.prepend(item)
menu.show_all()
@toggle_action(_('Bookmarks'), stock='zim-add-bookmark',
tooltip = 'Show/Hide Bookmarks', accelerator = BM_TOGGLE_BAR_KEY) # T: menu item bookmark plugin
def toggle_show_bookmarks(self, active):
'''
Show/hide the bar with bookmarks.
'''
if active:
self.show_widget()
else:
self.hide_widget()
self.uistate['show_bar'] = active
@action(_('Add Bookmark'), accelerator = BM_ADD_BOOKMARK_KEY) # T: menu item bookmark plugin
def add_bookmark(self):
'''
Function to add new bookmarks to the bar.
Introduced to be used via keyboard shortcut.
'''
self.widget.add_new_page()
class BookmarkBar(gtk.HBox, ConnectorMixin):
def __init__(self, ui, uistate, get_page_func):
gtk.HBox.__init__(self)
self.ui = ui
self.uistate = uistate
self.save_flag = False # if True save bookmarks in config
self.add_bookmarks_to_beginning = False # add new bookmarks to the end of the bar
self.max_bookmarks = False # maximum number of bookmarks
self._get_page = get_page_func # function to get current page
# Create button to add new bookmarks.
self.plus_button = IconsButton(gtk.STOCK_ADD, gtk.STOCK_REMOVE, relief = False)
self.plus_button.set_tooltip_text(_('Add bookmark/Show settings'))
self.plus_button.connect('clicked', lambda o: self.add_new_page())
self.plus_button.connect('button-release-event', self.do_plus_button_popup_menu)
self.pack_start(self.plus_button, expand = False)
# Create widget for bookmarks.
self.container = ScrolledHBox()
self.pack_start(self.container, expand = True)
# Toggle between full/short page names.
self.uistate.setdefault('show_full_page_name', False)
# Save path to use later in Copy/Paste menu.
self._saved_bookmark = None
self.paths = [] # list of bookmarks as string objects
self.uistate.setdefault('bookmarks', [])
# Add pages from config to the bar.
for path in self.uistate['bookmarks']:
page = self.ui.notebook.get_page(Path(path))
if page.exists() and (page.name not in self.paths):
self.paths.append(page.name)
self.paths_names = {} # dict of changed names of bookmarks
self.uistate.setdefault('bookmarks_names', {})
# Function to transform random string to paths_names format.
self._convert_path_name = lambda a: ' '.join(a[:25].split())
# Add alternative bookmark names from config.
for path, name in self.uistate['bookmarks_names'].iteritems():
if path in self.paths:
try:
name = self._convert_path_name(name)
self.paths_names[path] = name
except:
logger.error('BookmarksBar: Error while loading path_names.')
# Look for new pages to mark corresponding bookmarks in the bar.
self.connectto(self.ui, 'open-page', self.on_open_page)
# Delete a bookmark if a page is deleted.
self.connectto(self.ui.notebook.index, 'page-deleted',
lambda obj, path: self.delete(path.name))
def on_open_page(self, ui, page, path):
'''If a page is present as a bookmark than select it.'''
pagename = page.name
for button in self.container.get_children()[2:]:
if button.zim_path == pagename:
button.set_active(True)
else:
button.set_active(False)
def add_new_page(self, page = None):
'''
Add new page as bookmark to the bar.
:param page: L{Page}, if None takes currently opened page,
'''
if not page:
page = self._get_page()
if page.exists():
return self._add_new(page.name, self.add_bookmarks_to_beginning)
def _add_new(self, path, add_bookmarks_to_beginning = False):
'''Add bookmark to the bar.
:param path: path as a string object
:param add_bookmarks_to_beginning: bool,
add new bookmarks to the beginning of the bar,
'''
if path in self.paths:
logger.debug('BookmarksBar: path is already in the bar.')
self.plus_button.blink()
return False
# Limit max number of bookmarks.
if self.max_bookmarks and (len(self.paths) >= self.max_bookmarks):
logger.debug('BookmarksBar: max number of bookmarks is achieved.')
return False
# Add a new bookmark to the end or to the beginning.
if add_bookmarks_to_beginning:
self.paths.insert(0, path)
else:
self.paths.append(path)
self._reload_bar()
def delete(self, path):
'''
Remove one button from the bar.
:param path: string corresponding to Path.name.
'''
if path in self.paths:
self.paths.remove(path)
self.paths_names.pop(path, None)
self._reload_bar()
def delete_all(self, ask_confirmation = False):
'''
Remove all bookmarks.
:param ask_confirmation: to confirm deleting.
'''
def _delete_all():
self.paths = []
self.paths_names = {}
self._reload_bar()
if ask_confirmation:
# Prevent accidental deleting of all bookmarks.
menu = gtk.Menu()
item = gtk.MenuItem(_('Do you want to delete all bookmarks?')) # T: message for bookmark plugin
item.connect('activate', lambda o: _delete_all())
menu.append(item)
menu.show_all()
menu.popup(None, None, None, 3, 0)
else:
_delete_all()
def change_bookmark(self, old_path, new_path = None):
'''
Change path in bookmark from 'old_path' to 'new_path'.
:param new_path, old_path: strings corresponding to Path.name.
If 'new_path' == None takes currently opened page.
'''
if not new_path:
page = self._get_page()
if page.exists():
new_path = page.name
if new_path and (new_path not in self.paths) and (new_path != old_path):
self.paths[self.paths.index(old_path)] = new_path
name = self.paths_names.pop(old_path, None)
if name:
self.paths_names[new_path] = name
self._reload_bar()
else:
self.plus_button.blink()
def move_bookmark(self, first, second, direction):
'''
Move bookmark 'first' to the place near the bookmark 'second'.
:param first, second: strings corresponding to Path.name.
:param direction: move 'first' bookmark to the 'left' or 'right' of the 'second'.
'''
if (first == second) or (direction not in ('left','right')):
return False
if (first in self.paths) and (second in self.paths):
self.paths.remove(first)
ind = self.paths.index(second)
if direction == 'left':
self.paths.insert(ind, first)
else: # direction == 'right'
self.paths.insert(ind + 1, first)
self._reload_bar()
def rename_bookmark(self, button):
'''
Change label of the button.
New name is taken from the clipboard.
If button's name has been changed before,
change it back to its initial state.
'''
_full, _short = button.zim_path, self._get_short_page_name(button.zim_path)
if button.get_label() in (_short, _full):
# Change the button to new name.
new_name = None
try:
# Take from clipboard.
new_name = self._convert_path_name(Clipboard.get_text())
except:
logger.error('BookmarksBar: Error while converting from buffer.')
if new_name:
self.paths_names[_full] = new_name
button.set_label(new_name)
else:
# Change the button back to its initial state.
new_name = _full if self.uistate['show_full_page_name'] else _short
button.set_label(new_name)
self.paths_names.pop(_full, None)
if self.save_flag:
self.uistate['bookmarks_names'] = self.paths_names
def do_plus_button_popup_menu(self, button, event):
'''Handler for button-release-event, triggers popup menu for plus button.'''
if event.button == 3:
menu = gtk.Menu()
item = gtk.CheckMenuItem(_('Show full Page Name')) # T: menu item for context menu
item.set_active(self.uistate['show_full_page_name'])
item.connect('activate', lambda o: self.toggle_show_full_page_name())
menu.append(item)
menu.show_all()
menu.popup(None, None, None, 3, 0)
return True
def do_bookmarks_popup_menu(self, button, event):
'''Handler for button-release-event, triggers popup menu for bookmarks.'''
if event.button != 3:
return False
path = button.zim_path
_button_width = button.size_request()[0]
direction = 'left' if (int(event.x) <= _button_width/2) else 'right'
def set_save_bookmark(path):
self._saved_bookmark = path
if button.get_label() in (path, self._get_short_page_name(path)):
rename_button_text = _('Set New Name') # T: button label
else:
rename_button_text = _('Back to Original Name') # T: button label
# main popup menu
main_menu = gtk.Menu()
main_menu_items = (
(_('Remove'), lambda o: self.delete(path)), # T: menu item
(_('Remove All'), lambda o: self.delete_all(True)), # T: menu item
('separator', ''),
('gtk-copy', lambda o: set_save_bookmark(path)),
('gtk-paste', lambda o: self.move_bookmark(self._saved_bookmark, path, direction)),
('separator', ''),
(_('Open in New Window'), lambda o: self.ui.open_new_window(Path(path))), # T: menu item
('separator', ''),
(rename_button_text, lambda o: self.rename_bookmark(button)),
(_('Set to Current Page'), lambda o: self.change_bookmark(path)) ) # T: menu item
for name, func in main_menu_items:
if name == 'separator':
item = gtk.SeparatorMenuItem()
else:
if 'gtk-' in name:
item = gtk.ImageMenuItem(name)
else:
item = gtk.MenuItem(name)
item.connect('activate', func)
main_menu.append(item)
main_menu.show_all()
main_menu.popup(None, None, None, 3, 0)
return True
def on_bookmark_clicked(self, button):
'''Open page if a bookmark is clicked.'''
self.ui.open_page(Path(button.zim_path))
def on_preferences_changed(self, preferences):
'''Plugin preferences were changed.'''
self.save_flag = preferences['save']
self.add_bookmarks_to_beginning = preferences['add_bookmarks_to_beginning']
if self.save_flag:
self.uistate['bookmarks'] = self.paths
self.uistate['bookmarks_names'] = self.paths_names
else:
self.uistate['bookmarks'] = []
self.uistate['bookmarks_names'] = {}
if self.max_bookmarks != preferences['max_bookmarks']:
self.max_bookmarks = preferences['max_bookmarks']
self._reload_bar() # to update plus_button
def _get_short_page_name(self, name):
'''
Function to return short name for the page.
Is used to set short names to bookmarks.
'''
path = Path(name)
return path.basename
def toggle_show_full_page_name(self):
'''Change page name from short to full and vice versa.'''
self.uistate['show_full_page_name'] = not self.uistate['show_full_page_name']
self._reload_bar()
def _reload_bar(self):
'''Reload bar with bookmarks.'''
for button in self.container.get_children()[2:]:
self.container.remove(button)
page = self._get_page()
if page:
pagename = page.name
else:
pagename = None
for path in self.paths:
if path in self.paths_names:
name = self.paths_names[path]
elif not self.uistate['show_full_page_name']:
name = self._get_short_page_name(path)
else:
name = path
button = gtk.ToggleButton(label = name, use_underline = False)
button.set_tooltip_text(path)
button.zim_path = path
if path == pagename:
button.set_active(True)
button.connect('clicked', self.on_bookmark_clicked)
button.connect('button-release-event', self.do_bookmarks_popup_menu)
button.show()
self.container.add(button)
# 'Disable' plus_button if max bookmarks is reached.
if self.max_bookmarks and (len(self.paths) >= self.max_bookmarks):
self.plus_button.change_state(False)
else:
self.plus_button.change_state(True)
# Update config files.
if self.save_flag:
self.uistate['bookmarks'] = self.paths
self.uistate['bookmarks_names'] = self.paths_names
class IconsButton(gtk.Button):
'''
Need a button which can change icons.
Use this instead of set_sensitive to show 'disabled'/'enabled' state
because of the need to get signal for popup menu.
For using only with one icon look for the standard IconButton from widgets.py.
'''
def __init__(self, stock_enabled, stock_disabled, relief=True, size=gtk.ICON_SIZE_BUTTON):
'''
:param stock_enabled: the stock item for enabled state,
:param stock_disabled: the stock item for disabled state,
:param relief: when C{False} the button has no visible raised,
edge and will be flat against the background,
:param size: the icons size
'''
gtk.Button.__init__(self)
self.stock_enabled = gtk.image_new_from_stock(stock_enabled, size)
self.stock_disabled = gtk.image_new_from_stock(stock_disabled, size)
self.add(self.stock_enabled)
self._enabled_state = True
self.set_alignment(0.5, 0.5)
if not relief:
self.set_relief(gtk.RELIEF_NONE)
def change_state(self, active = 'default'):
'''
Change icon in the button.
:param active: if True - 'enabled', False - 'disabled',
if 'default' change state to another.
'''
if active == 'default':
active = not self._enabled_state
if active != self._enabled_state:
self.remove(self.get_child())
if active:
self.add(self.stock_enabled)
else:
self.add(self.stock_disabled)
self._enabled_state = not self._enabled_state
self.show_all()
def blink(self):
'''Quickly change an icon to show
that bookmark can't be added/changed.'''
def change_icon():
'''Function to be called only once.'''
self.change_state()
return False
self.change_state()
gobject.timeout_add(300, change_icon)