-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathshelf_browser.py
More file actions
1573 lines (1289 loc) · 54.1 KB
/
shelf_browser.py
File metadata and controls
1573 lines (1289 loc) · 54.1 KB
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
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
NAME
shelf_browser - a small GUI to view contents of shelve files (read only)
DESCRIPTION
This is a small project primarily undertaken for the purpose of learning
tkinter but solving a real need to quickly access and view the
contents of shelf files created by the shelve module. The application
is currently meant purely to provide a quick read-only look at the
contents and not to offer more extensive interactions with the shelf file.
The classes contained within are meant to be used together to form the
whole application but where possible I have built them so they are
independent enough to use as components in future projects.
Application Functionality:
- Browse to and open shelf file (File --> Open...)
- Quick reference to name of currently opened shelf file
- View details of open shelf file (File --> File Info...)
- create copy of current shelf in dbm.dumb format for portability
(File--> File Info --> "Convert to portable format")
- Scrollable list of shelf keys provided in left hand pane (listbox)
- Keys are sorted alphabetically prior to being displayed
- Listbox displays key entries in alternating bg colour
- R-click or ctrl-c to copy key text to clipboard
- Filter shelf keys based on search criteria typed into search entry
- Disable filter (show all key, ignoring what is in the search entry)
- View summary information about shelf keys in info bar at bottom of list
- how many shelf keys in file
- how many are being displayed (ie. not filtered)
- which position key is currently selected
- Left-click listbox item to display details of value stored for the
key in right hand pane
- Press delete to delete key/value entry from shelf (one at a time)
- Pbject type and preview of value displayed in summary pane (top right)
- Detailed string version of value displayed in scrolling text box
which is read-only (right pane).
- If value is a list or tuple, the text box is broken down into sections
with further details of the sub item object type and content.
(one level only for now and no tree view yet.)
- Right-click to jump between different sections if list or tuple output
has been handled
- Highlight search value matches for strings entered in search entry
- Display x/x search results statistics on floating pane.
- Close floating search stats pane by clicking red x
- Iterate through matched search results either forward or backwards
- Press enter on search entry box to iterate forwards through matches
- 'Current' search match is highlighted a different colour
- Some intitive interactions enabled on scrolled text
(ctrl-a, click drag to highlight, left-click to deselect)
- R-click to copy highlighted text from scrolled text
- Font size interaction, (indepentent for left and right frames
in order to be able to achieve different text sizes:
ctrl-+ increase font size by 1.
ctrl-- decrease font size by 1.
ctrl-0 reset font size to 8
- Set font size via menu (applies to ALL fonts in application)
(Edit --> Font)
- Cut, Copy, Paste r-click context menu for search entry widget
- Exit application (File --> Exit)
Included Classes:
DefaultContextMenu(tk.Menu)
Provide a default r-click style context menu for cut, copy paste
ShelfBrowser()
Manage master tkinter frame and application layout
FindEntry()
Provide Entry box for text searches. For reference by other classes
ShelfManager()
Manage shelve file interaction and present shelve.keys() in listbox
ResultsPane()
Present shelf value content in text format in tkinter Text()
Useful Functions:
set_fonts()
Change font size for a tk.Font() object
font_events()
Increases or decreases size of fonts on ctrl-+, ctrl--, ctrl-0
goto_url()
Open URL passed by calling program
url_cursor()
Change cursor type to hand2 for rolling over urls
print_key()
print the key generating the event to stdout
set_widget_focus()
Set current focus on widget generating the event
tktext_cancel_sel()
Cleanly removes highlighted selection for tk.Text() widgets
tkcopy()
Copy highlighted text to clipboard
tkcut()
Copy highlighted text to clipboard
tkpaste()
Paste clipboard text to widget
tk_selectall()
Handle ctrl-a event to select all
'''
import math
import os
import stat #for interpreting os.stat results
from functools import partial
from time import asctime, localtime, strftime
import shelve
import requests
from bs4 import BeautifulSoup
import webbrowser
from dbm import whichdb, dumb
import tkinter as tk
import tkinter.font as tkfnt
from tkinter import filedialog
from tkinter import messagebox
from tkinter import scrolledtext
from tkinter import ttk
# Helper functions and event handlers not widget or class specific.
def set_fonts(fnt, f_size):
'''Change font size for a tk.Font() object'''
fnt.configure(size=f_size)
return 'break'
def font_events(evt, fnt_grp,*, f_size=None):
'''
Increases or decreases size of fonts on ctrl-+, ctrl--, ctrl-0
Expects a list object with any number of sub lists of the makeup
[<font object>, <size offset>]. The former is a tk.Font() object that
will be altered, the latter is a size offset relative to the f_size
parameter which must be passed if ctrl-0 is received.
Note that f_size MUST be passed in the case of a Ctrl-0 event.
Parameters:
fnt_grp : List
entries must be sublists of format [<font object>, <size offset>]
list of tk.Font() objects to update with size
f_size : int
in the case of Ctrl-0, size of font to adjust to
'''
for f in fnt_grp:
if evt.keysym in ['plus', 'equal']:
f[0].configure(size=f[0].cget('size')+1)
elif evt.keysym == 'minus':
f[0].configure(size=f[0].cget('size')-1)
elif evt.keysym == '0':
f[0].configure(size=f_size+f[1]) # no error catch in case f_size missed. we want the error.
return 'break'
def goto_url(evt, url):
'''Open URL passed by calling program'''
webbrowser.open(url)
return 'break'
def url_cursor(evt):
'''Change cursor type to hand2 for rolling over urls'''
if evt.type == '7': #7 = '<Enter>' event
evt.widget.config(cursor='hand2')
else: #assume this will always be event type #8 '<Leave>'
evt.widget.config(cursor='')
return 'break'
def print_key(evt):
'''print the key generating the event to stdout'''
print(evt.keysym)
return 'break'
def set_widget_focus(evt):
'''Set current focus on widget generating the event'''
evt.widget.focus_set()
return 'break'
def tktext_cancel_sel(evt):
'''Cleanly removes highlighted selection for tk.Text() widgets'''
if not isinstance(evt.widget, tk.Text):
return 'break'
evt.widget.tag_remove(tk.SEL, 1.0, tk.END)
evt.widget.mark_unset('tk::anchor1')
evt.widget.mark_set(tk.INSERT, tk.CURRENT)
def tkcopy(evt=None, widget=None):
'''Copy highlighted text to clipboard'''
widget = widget or evt.widget # or acts as coalesce here
if isinstance(widget, (tk.Text, tk.Entry)):
try:
widget.clipboard_clear()
widget.clipboard_append(widget.selection_get())
except:
pass
elif isinstance(widget, tk.Listbox):
try:
widget.clipboard_clear()
widget.clipboard_append(widget.get(widget.curselection()[0]))
except:
widget.clipboard_append('')
return 'break'
def tkcut(evt=None, widget=None):
'''Copy highlighted text to clipboard'''
widget = widget or evt.widget # or acts as coalesce here
if isinstance(widget, (tk.Text, tk.Entry)):
try:
widget.clipboard_clear()
widget.clipboard_append(widget.selection_get())
widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
except Exception as err:
pass
return 'break'
def tkpaste(evt=None, widget=None):
'''Paste clipboard text to widget'''
widget = widget or evt.widget # or acts as coalesce here
if isinstance(widget, (tk.Text, tk.Entry)):
widget.focus()
widget.insert(tk.INSERT, widget.clipboard_get())
try: # try to delete any current selection
widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
except Exception as err:
pass
return 'break'
def tk_selectall(evt=None, widget=None):
'''Handle ctrl-a event to select all'''
widget = widget or evt.widget #or acts as coalesce here
if isinstance(widget, tk.Text):
widget.tag_add('sel', '1.0', 'end')
widget.mark_set(tk.INSERT, 1.0)
elif isinstance(widget, tk.Entry):
widget.select_range(0, 'end')
return 'break'
class DefaultContextMenu(tk.Menu):
'''
Provide a default r-click context menu with copy/paste capability.
Inherits from tk.Menu() object.
Attributes:
master : tkinter widget
widget over which the context menu will appear
__default_commands : dict
dictionary of default commands to be included in the menu
and their current status (True=show, False=hide). Set with
self.setup().
Methods:
popup (self, evt)
triggered by an event call from the master widget. posts
the menu at the coordinates of the mouse.
popupFocusOut(self, evt):
hide the menu when the user clicks away
setup(self, **kwargs)
updates the show/hide status of the default menu items and
populates the menu with default options. Call with
dfcopy/dfpaste/dfcancel = True/False.
reset(self)
calls setup(). this is simply to provide a more readable call
for the setup function when resetting the menu during a
program.
'''
def __init__(self, master, cnf={}, **kw):
'''See tkinter.Menu() docs'''
tk.Menu.__init__(self, master, cnf, **kw)
self.master = master
self.__default_commands = {'dfcopy':True, 'dfcut': True, 'dfpaste':True, 'dfcancel':True}
self.tearoff = 0
self.bind("<FocusOut>", self.popupFocusOut)
self.setup()
def popup(self, evt):
'''Posts the menu at the coordinates of the mouse'''
try:
self.post(evt.x_root, evt.y_root)
self.focus() #need to set focus in order for <FocusOut> to work.
finally:
self.grab_release()
return 'break'
def popupFocusOut(self, evt):
'''Hides menu when user clicks away'''
self.unpost()
return 'break'
def setup(self, **kwargs):
'''
Update default menu items to show and redraw menu.
Parameters:
**kwargs
valid values: dfcopy=True/False Copy
dfpaste=True/False Paste
dfcancel=True/False Cancel
'''
for c in self.__default_commands.keys():
u = kwargs.get(c)
if type(u) == bool:
self.__default_commands[c] = u
self.delete(0, tk.END)
if self.__default_commands.get('dfcopy'):
self.add_command(label='Copy', command=partial(tkcopy, evt=None, widget=self.master))
if self.__default_commands.get('dfcut'):
self.add_command(label='Cut', command=partial(tkcut, evt=None, widget=self.master))
if self.__default_commands.get('dfpaste'):
self.add_command(label='Paste', command=partial(tkpaste, evt=None, widget=self.master))
if self.__default_commands.get('dfcancel'):
self.add_command(label='Cancel')
def reset(self):
'''Call self.setup()'''
self.setup()
class ShelfBrowser():
'''
Master class to manage window layout, fonts and menu bar.
ShelfBrowser initiates and sets overall app layout, initiates
other classes which contain program functionality and controls
basic menu functions. This class also sets the default font
from which overall application look feel is derived by other
classes.
Call this from __main__ with a Tk() object as arguement.
Eg.
>>> import tkinter as tk
>>> root = tk.Tk()
>>> ShelfBrowser(root)
>>> root.mainloop()
Parameters:
see __init__ doc string.
Attributes:
root : tk.Tk()
primary window object under which all further frames will be
created
main_font : tk.Font()
the default font to be used throughout the application
Methods:
__open_shelf()
Called from the File menu.
Opens a file-selection dialog and passes a selected
file path to the ShelfManager instance.
__fontsize(f_size):
Called as command arguement from fontsize menu with the
selected font size as a parameter. Subsequently calls
font resize methods of other classes as needed.
__file_info():
Called as command arguement from File menu.
Creates a custom transient dialog window in which to display
details about the currently open file. File details are
requested by calling ShelfManager.get_shelf_details('p')
Also creates a command button to activate
ShelfManager.convert_to_dumb()
Note: The construction of this dialog should perhaps have been
managed entirely by the ShelfManager class but I decided to
leave menu creation in the hands of this class and push the
heavy lifting of determining the file detils to ShelfManager.
I'm still wondering whether to change it.
'''
def __init__(self, parent):
'''
Create window layout, font, menu and call appication classes.
Parameters:
parent : tk.Tk()
root tkinter window in which to build application.
'''
#define main font/s
self.__fonts = []
self.main_font = tkfnt.Font(size=8)
self.__fonts.append(self.main_font)
#create root window
self.root = parent
self.root.title('Shelf Browser Utility - 1.0')
self.root.geometry('1300x800')
self.root.rowconfigure(1, weight=1)
self.root.columnconfigure(0, weight=1)
#add paned window to hold 2 main application frames
self.pwin = tk.PanedWindow(self.root)
self.pwin.grid(column=0, row=1, sticky=(tk.N,tk.S,tk.E,tk.W))
#add frames window to act as root for application classes
#frame0 - search box (FindEntry() class)
self.frame0 = tk.Frame(self.root)
self.frame0.grid(column=0, row=0, sticky=(tk.N,tk.S,tk.E,tk.W))
self.frame0.rowconfigure(0, weight=1)
self.frame0.columnconfigure(0, weight=1)
#frame1 - paned window left frame list box (ShelfManager() class)
self.frame1 = tk.Frame(self.pwin)
self.frame1.rowconfigure(1, weight=1)
self.frame1.columnconfigure(0, weight=1)
self.pwin.add(self.frame1, width=650)
#frame2 - paned window right frame text box (ResultsPane() class)
self.frame2 = tk.Frame(self.pwin)
self.frame2.rowconfigure(1, weight=1)
self.frame2.columnconfigure(0, weight=1)
self.pwin.add(self.frame2)
#construct menus
#master menu
self.men1 = tk.Menu(self.pwin, font=self.main_font, tearoff=0)
#File Menu
self.file_menu = tk.Menu(self.men1, font=self.main_font, tearoff=0)
self.file_menu.add_command(label='Open...', command=self.__open_shelf)
self.file_menu.add_command(label='File info...', command=self.__file_info)
self.file_menu.add_separator()
self.file_menu.add_command(label='Exit', command=self.root.destroy)
self.men1.add_cascade(label='File', menu=self.file_menu)
#Edit menu
self.edit_menu = tk.Menu(self.men1, font=self.main_font, tearoff=0)
self.fonts_menu = tk.Menu(self.edit_menu, font=self.main_font, tearoff=0)
for f in [4,6,8,10,12,14,16,18,20]:
self.fonts_menu.add_command(label=f, command=partial(self.__fontsize, f_size=f))
self.edit_menu.add_cascade(label='Font', menu=self.fonts_menu)
self.men1.add_cascade(label='Edit', menu=self.edit_menu)
#add completed menu to root window
self.root.config(menu=self.men1)
#add search box (frame0)
self.findentry = FindEntry(self.frame0, self.main_font)
#add results pane (frame2)
self.resultspane = ResultsPane(self.frame2, self.main_font, self.findentry)
#add list box (frame1)
self.shelfmanager = ShelfManager(self.frame1, self.main_font, self.resultspane, self.findentry)
def __open_shelf(self):
'''
Ask user to select a file and pass path to ShelfManager class
'''
try:
path_ = ''
path_ = filedialog.askopenfilename()
if not os.path.isfile(path_):
raise Exception('os.path.isfile() returns False')
self.shelfmanager.populate(path_)
except Exception as err:
errmsg = 'Error selecting file.\n\nException of type {0} occurred.\n{1!r}'.format(type(err).__name__, err.args)
messagebox.showwarning('File selection error...', errmsg)
return 'break'
def __fontsize(self, f_size):
'''
Alter font size for all applicable fonts within current self.fonts
This method resizes fonts within the current instance based on
the input parameter and then calls the font resize method of any
additional objects which manage their own fonts if required.
Parameters:
f_size : Int
integer determining new font size to apply
'''
for f in self.__fonts:
f.configure(size=f_size)
self.resultspane.resize_fonts(f_size)
def __file_info(self):
'''
Create custom dialog window and fill it with output from ShelfManager.
Creates a custom dialog window using tk.Toplevel() and populates
it with details of the currently open file being managed by a
ShelfManager object. The intention of this method is to provide
the layout/presentation window while ShelfManager takes care of
interrogating the file.
The ShelfManager.get_shelf_details() method is called to retreive
the relevant file information. Passing the 'p' value as
parameter requests a printable list rather than raw attribute
data.
'''
self.info_dialog = tk.Toplevel()
self.info_dialog.title('File info...')
self.info_dialog.transient(self.root)
shelf_detail = self.shelfmanager.get_shelf_details('p')
txt1 = ''
txt2 = ''
for i in shelf_detail:
txt1 += '{}:\r\n'.format(i[0])
txt2 += '\t{}\r\n'.format(i[1])
self.info_dialog_lbl1 = tk.Label(self.info_dialog, text=txt1, justify=tk.LEFT, font=self.main_font)
self.info_dialog_lbl1.grid(row=0, column=0, ipadx=20, ipady=20)
self.info_dialog_lbl2 = tk.Label(self.info_dialog, text=txt2, justify=tk.LEFT, font=self.main_font)
self.info_dialog_lbl2.grid(row=0, column=1, ipadx=20, ipady=20)
self.info_dialog_close = tk.Button(self.info_dialog,
text='Close',
font=self.main_font,
command=self.info_dialog.destroy)
self.info_dialog_close.grid(row=10, column=0, columnspan=2, padx=10, pady=10)
self.info_dialog_convert = tk.Button(self.info_dialog,
text='Convert to portable format (dbm.dumb)',
font=self.main_font,
bg='red',
command=self.shelfmanager.convert_to_dumb)
self.info_dialog_convert.grid(row=11, column=0, columnspan=2, padx=20, pady=20)
class FindEntry():
'''
Search box object intended for re-use by multiple other objects.
FindEntry() provides a tk.Entry widget for the user to type
a search string into. It is intended to offer a single point
of entry to the user and therefore to be interrogated by
multiple parts of the application as part of their independent
search / filter methods.
Parameters:
see __init__ doc string.
Attributes:
root : tkinter window object (multiple)
a frame or window in which to place the Entry widget
active : tk.BooleanVar()
1 = instance is being used by the user to search
0 = instance is inactive (not in use)
search_string : tk.StringVar()
string value representing current value of Entry
widget. When active=1, this is the value of
the user's text search inquiry. When active=0
this is the default text on the widget.
main_font : tk.Font()
default font used by the widget, defined by from
calling entity
Methods:
__usage_indicator(self, args*)
triggered by <FocusIn> and <FocusOut> events.
sets the active attribute on or off depending on
whether a search value is entered.
sets the value of the Entry widget to default
when not in use.
removes the default text from Entry widget
when moving from inactive to active state
__ctrl_backspce(self, evt)
deletes all text from the widget on Control-Backspace
'''
def __init__(self, parent, fnt=None):
'''
Initiate search box objects, define layout and set event bindings.
Parameters:
parent : tk.Tk()
frame or Tk() window in which to place Entry widget
fnt : tk.Font()
a default font for use or derivation throughout.
if fnt=None, a default font of size 8 will be defined.
'''
self.root = parent
self.active = tk.BooleanVar()
self.search_string = tk.StringVar()
self.__default_text = '<search here>'
self.active.set(0)
#setup fonts
self.__fonts = []
if fnt == None:
self.main_font = tkfnt.Font(size=8)
else:
self.main_font = fnt
#setup entry box
self.search_string.set(self.__default_text)
self.find_entry = tk.Entry(self.root, font=self.main_font, textvariable=self.search_string, border=2, fg='grey')
self.find_entry.grid(column=0, row=0, sticky=(tk.N,tk.S,tk.E,tk.W))
#setup event bindings
self.find_entry.bind('<FocusIn>', self.__usage_indicator)
self.find_entry.bind('<FocusOut>', self.__usage_indicator)
self.find_entry.bind('<Control-a>', tk_selectall)
self.find_entry.bind('<Control-BackSpace>', self.__ctrl_backspce)
#popup menu
self.context_menu = DefaultContextMenu(self.find_entry, tearoff=0, font=self.main_font)
self.find_entry.bind('<Button-3>', self.context_menu.popup)
def __usage_indicator(self, *args):
''' Set self.active indicator. See class doc string for details.'''
if len(self.find_entry.get()) == 0:
self.active.set(0)
self.search_string.set(self.__default_text)
elif self.active.get() == 0:
self.search_string.set('')
self.active.set(1)
return 'break'
#event handlers
def __ctrl_backspce(self, evt):
''' Event handler. Delete contents of search box when triggered.'''
evt.widget.delete(0, tk.END)
return 'break'
class ShelfManager():
'''
Open shelf file, display keys and handle user interactions.
This class creates a tk.Listbox object and populates it with the keys
present in a shelve.Shelf(). It's primary purpose is to manage
opening of the shelf file, displaying the key entries within and
passing values to a separate display handler. At the time of writing
the display handler is intended to be a ResultsPane() object but
this ShelfManager could easily be used with a different object with
very minor corrections to the sendto_target() method.
The class also offers functionality to delete an entry from a shelf
file and to return file system info about the opened file.
Attributes:
root : tk.Tk() or other tkinter frame object
a frame or window in which to place the Entry widget
current_shelf : str
path of current shelf file.
This is separate to __selected_shelf_file because some
formats of shelf (eg. dbm.dumb) are not called by filename
but rather by a general shelf name.
(eg shelve.open('/test' may refer to /test.bak, /test.dat etc)
main_font : tk.Font()
default font used by the widget, defined by from
calling entity
shelfkeys : List
full list of keys extracted from the 'current' (most recently
opened) shelf file.
search_check_val : tk.IntVar()
current value of the 'disable filter' checkbox 0=off; 1=on.
a trace is setup on this value to react to any changes.
target_pane : ResultsPane() or other text display handler
optional default display handler to which to send values.
used by send_to_target
target_find : FindEntry()
instance of FindEntry() passed as a parameter when instantiating.
this object will be tracked for search/filter functionality.
__selected_shelf_file : str
path of the specific file selected by the user. This may differ
to the value of current_shelf. (See current_shelf doc above.)
Methods:
populate(self, filepath)
open shelf file passed as parameter and populate listbox with
keys. Also populates self.current_shelf and
self.__selected_shelf_file
reset_list(self, populate=False)
delete all current entries from key listbox.
if populate=True is passed, will repopulate lisbox with all
keys from self.shelfkeys variable.
colour_items(self)
add alternate colouring for lines in listbox.
in a future enhancement this could be updated to make the
colours configurable by the user.
set_infobar(self, *args)
add details to the information bar which is placed at the
bottom of the frame. This is called whenever the listbox
changes (<<ListboxSelect>>) or by other functions which
alter the state of the listbox.
Info displayed:
Total number of items in self.shelfkeys
Total number of items currently in listbox (in case of filter)
Current index number selected in
filter_list(self, *args)
filters the main listbox based on changes to the search criteria
entered into the FindEntry() object defined in the target_find()
parameter. If the searchbox is not active or if the
'disable filter' checkbox is checked, the function will
populate the listbox with the full contents of self.shelfkeys.
this function could be updated in the future to take filter input
from other sources than the target_find object.
selected_key(self)
return index of currently selected item. included simply to make
the code more readable.
sendto_target(self, target=None)
retreives the shelf value for the associated input key and calls
a display handler, passing the value content as a parameter.
Called either by a <<ListboxSelect>> event or by
<Double-Button-1> event.
by default, the function will call the .populate() method of
the object passed as target_pane but if target_pane=None
the function will create a tk.Toplevel() window and create
new ResultsPane() and FindEntry() instances to populate it
before passing the value content to this new object.
get_shelf_details(self, mode='p') -> List or -> Dict
returns relevant file details of the currently opened
shelf file using os.stat and dbm.whichdb.
mode='p' will return a formatted list of values ready for display
mode='d' will return a dictionary containing raw values
details returned are:
- file name
- directory name
- full path
- dbm schema (dbm.dumb, dbm.gnu etc)
- file size
- created
- last modified
- last accessed
convert_to_dumb(self)
if the current shelf file is not already dbm.dumb format, this
method creates a copy in dbm.dumb format. The purpose of this
is to enable the user to create a cross-platform compatible
version of the shelf if needed.
Event Handler Methods:
item_delete(self, evt) Trigger : <<Delete>>
when delete key is pressed, removes the key/value from current_shelf
corresponding to the currently selected key in listbox.
item_select(self, evt) Trigger : <<ListboxSelect>>
calls the sendto_target() function with target_pane specified.
expand_result(self, evt) Trigger : <Return>
calls the sendto_target() function with no target_pane specified
forcing the application to create a separate popup window to
pass the value to.
search_check_change(self, *args) Trigger : self.search_check_val.trace()
trigger filtering of the listbox in case "disable filter"
checkbox is turned on.
'''
def __init__(self, parent, fnt=None, target_pane=None, target_find=None):
'''
Initiate ShelfManager() objects, define layout and set event bindings.
Parameters:
parent : tk.Tk()
frame or Tk() window in which to place listbox and other widgets
fnt : tk.Font()
a default font for use or derivation throughout.
if fnt=None, a default font of size 8 will be defined.
target_pane : ResultsPane() or other display handler
object to call when passing shelve values for display.
assume ResultsPane() object here but any other object with a
.populate method is acceptable.
target_find : FindEntry()
FindEntry() object which will be accessed for filter criteria.
'''
self.root = parent
self.current_shelf = '<no shelf to display>'
self.__selected_shelf_file = ''
self.target_pane = target_pane
self.shelfkeys = []
self.search_check_val = tk.IntVar()
self.search_check_val.trace('w', self.search_check_change)
#setup fonts
self.fonts = []
if fnt == None:
self.main_font = tkfnt.Font(size=8)
else:
self.main_font = fnt
#if search entry is defined, setup trigger to capture change events
if target_find:
self.target_find = target_find
self.search_string = self.target_find.search_string
self.search_string.trace('w', self.filter_list)
#create label showing current file
self.file_label = tk.Label(self.root, text=self.current_shelf, fg='white', bg='grey', anchor='w', font=('Ariel', 6, 'bold'))
self.file_label.grid(column=0, row=0, sticky=(tk.N,tk.S,tk.E,tk.W))
#create search check box
self.search_check = tk.Checkbutton(self.root, text='disable search', variable=self.search_check_val, onvalue=1, offvalue=0, fg='#D3D3D3', bg='grey', highlightthickness=0, justify='center', font=('Ariel', 6))
self.search_check.grid(column=0, row=0, sticky=tk.E)
#create main list box
self.keylist = tk.Listbox(self.root, font=self.main_font, exportselection=False)
self.keylist.grid(column=0, row=1, sticky=(tk.N,tk.S,tk.E,tk.W))
self.keylist.activate(0)
self.keylist.bind('<<ListboxSelect>>', self.set_infobar)
self.keylist.bind('<<ListboxSelect>>', self.item_select, add='+')
self.keylist.bind('<Double-Button-1>', self.expand_result)
self.keylist.bind('<Return>', self.expand_result)
self.keylist.bind('<Delete>', self.item_delete)
self.keylist.bind('<Control-plus>', partial(font_events, fnt_grp=[(self.main_font,0)] ))
self.keylist.bind('<Control-equal>', partial(font_events, fnt_grp=[(self.main_font, 0)] ))
self.keylist.bind('<Control-minus>', partial(font_events, fnt_grp=[(self.main_font, 0)] ))
self.keylist.bind('<Control-0>', partial(font_events, fnt_grp=[(self.main_font, 0)], f_size=self.main_font.cget('size') ))
self.keylist.bind('<Control-c>', partial(tkcopy, widget=self.keylist))
#create vertical scroll bar
self.v_scrl1 = tk.Scrollbar(self.root, orient='vertical')
self.v_scrl1.config(command=self.keylist.yview)
self.v_scrl1.grid(column=1, row=1, rowspan=2, sticky=(tk.N,tk.S,tk.E,tk.W))
#create horizontal scroll bar
self.h_scrl1 = tk.Scrollbar(self.root, orient='horizontal')
self.h_scrl1.config(command=self.keylist.xview)
self.h_scrl1.grid(column=0, row=2, sticky=(tk.N,tk.S,tk.E,tk.W))
#attach scrollbars
self.keylist.config(yscrollcommand=self.v_scrl1.set)
self.keylist.config(xscrollcommand=self.h_scrl1.set)
#create info bar
self.infobar = tk.Label(self.root, font=self.main_font, text='Showing 0 of 0 records\trecord 0 selected', anchor='w', bd=2, relief='sunken')
self.infobar.grid(column=0, row=3, columnspan=2, sticky=(tk.N, tk.S, tk.E, tk.W))
self.set_infobar()
#popup menu
self.context_menu = DefaultContextMenu(self.keylist, tearoff=0, font=self.main_font)
self.context_menu.setup(dfpaste=False, dfcancel=False, dfcut=False)
self.keylist.bind('<Button-3>', self.context_menu.popup)
def populate(self, filepath):
'''
Open shelf file specified in filepath and populate listbox with
keys found within. Also populate self.shelfkeys and
self.__selected_shelf_file.
If the path is not to a valid shelf, an error will be returned.
Parameters:
filepath : str
string containing path to a validshelf file.
'''
self.shelfkeys = []
self.__selected_shelf_file = filepath
if whichdb(os.path.splitext(filepath)[0]) != None or '':
self.current_shelf = os.path.splitext(filepath)[0] #make compatible with dumbdbm
else:
self.current_shelf = filepath
try:
with shelve.open(self.current_shelf) as s:
self.shelfkeys = [i for i in s]
self.shelfkeys.sort()
self.file_label.configure(text='Displaying shelf: ' + self.current_shelf)
self.reset_list(populate=True)
except Exception as err:
errmsg = 'Error opening file, please select a valid shelf file.\n\nException of type {0} occurred.\n{1!r}'.format(type(err).__name__, err.args)
messagebox.showwarning('File open error...', errmsg)
return 'break'
self.filter_list()
def reset_list(self, populate=False):
'''
Clear listbox and populate if parameter is set to True
Parameters:
populate : Bool
=False only clear the list (default)
=True populate list with full contents of self.shelfkeys
'''
self.keylist.delete(0, tk.END)
if populate == True:
self.keylist.insert(tk.END, *self.shelfkeys)
self.colour_items()
self.set_infobar()
return
def colour_items(self):
'''Colour items in listbox in alternate colours'''
l = range(0, self.keylist.size())
for i in l[0::2]:
self.keylist.itemconfig(i, bg='white')
for i in l[1::2]:
self.keylist.itemconfig(i, bg='#EBF4FA') #light blue
def set_infobar(self, *args):
'''Populate information bar with details about list items. See class doc.'''
if len(self.keylist.curselection()) == 0:
curselect = '0'
else:
curselect = self.keylist.curselection()[0] + 1
txt = 'Showing {0} of {1} records.\tRecord {2} selected.'.format(
self.keylist.size(),
len(self.shelfkeys),
curselect
)
self.infobar.config(text=txt)
def filter_list(self, *args):
'''Apply search filter to listbox. Can be triggered by event'''
search_text = self.search_string.get()
self.reset_list()
if len(search_text) == 0 or self.target_find.active.get() == 0 or self.search_check_val.get() == 1:
self.reset_list(populate=True) #insert all
else:
for i in self.shelfkeys:
if search_text in i:
self.keylist.insert(tk.END, i)
self.colour_items()
self.set_infobar()
return 'break'
def selected_key(self):
'''Return index of currently selected listbox item.'''
item_index = self.keylist.curselection()[0]
shelf_key = self.keylist.get(item_index)
return shelf_key
def sendto_target(self, target=None):
'''
Retreive value from the the current shelf file according to the
currently selected key in listbox. Call target display handler
with .populate command, passing the retreived value.
If no target display handler is passed as a parameter, construct
tk.Toplevel() window and create a ResultsPane() and FindEntry()
objects to pass the value to.
Parameters: