Passed
Pull Request — develop (#458)
by
unknown
02:49
created

doorstop.gui.main.Application.remove()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
#!/usr/bin/env python
2
3
"""Graphical interface for Doorstop."""
4
5
import sys
6
from unittest.mock import Mock
7
try:  # pragma: no cover (manual test)
8
    import tkinter as tk
9
    from tkinter import ttk
10
    from tkinter import font, filedialog
11
except ImportError as _exc:  # pragma: no cover (manual test)
12
    sys.stderr.write("WARNING: {}\n".format(_exc))
13
    tk = Mock()
14
    ttk = Mock()
15
import os
16
import argparse
17
import functools
18
from itertools import chain
19
import logging
20
21
from doorstop import common
22
from doorstop.common import HelpFormatter, WarningFormatter, DoorstopError
23
from doorstop.core import vcs
24
from doorstop.core import builder
25
from doorstop import settings
26
27
log = common.logger(__name__)
28
29
30
def main(args=None):
31
    """Process command-line arguments and run the program."""
32
    from doorstop import GUI, VERSION
0 ignored issues
show
introduced by
Import outside toplevel (doorstop)
Loading history...
33
34
    # Shared options
35
    debug = argparse.ArgumentParser(add_help=False)
36
    debug.add_argument('-V', '--version', action='version', version=VERSION)
37
    debug.add_argument('-v', '--verbose', action='count', default=0,
38
                       help="enable verbose logging")
39
    shared = {'formatter_class': HelpFormatter, 'parents': [debug]}
40
    parser = argparse.ArgumentParser(prog=GUI, description=__doc__, **shared)
41
42
    # Build main parser
43
    parser.add_argument('-j', '--project', metavar="PATH",
44
                        help="path to the root of the project")
45
46
    # Parse arguments
47
    args = parser.parse_args(args=args)
48
49
    # Configure logging
50
    _configure_logging(args.verbose)
51
52
    # Run the program
53
    try:
54
        success = run(args, os.getcwd(), parser.error)
55
    except KeyboardInterrupt:
56
        log.debug("program interrupted")
57
        success = False
58
    if success:
59
        log.debug("program exited")
60
    else:
61
        log.debug("program exited with error")
62
        sys.exit(1)
63
64
65
def _configure_logging(verbosity=0):
66
    """Configure logging using the provided verbosity level (0+)."""
67
    # Configure the logging level and format
68
    if verbosity == 0:
69
        level = settings.VERBOSE_LOGGING_LEVEL
70
        default_format = settings.VERBOSE_LOGGING_FORMAT
71
        verbose_format = settings.VERBOSE_LOGGING_FORMAT
72
    else:
73
        level = settings.VERBOSE2_LOGGING_LEVEL
74
        default_format = settings.VERBOSE_LOGGING_FORMAT
75
        verbose_format = settings.VERBOSE_LOGGING_FORMAT
76
77
    # Set a custom formatter
78
    logging.basicConfig(level=level)
79
    formatter = WarningFormatter(default_format, verbose_format)
80
    logging.root.handlers[0].setFormatter(formatter)
81
82
83
def run(args, cwd, error):
84
    """Start the GUI.
85
86
    :param args: Namespace of CLI arguments (from this module or the CLI)
87
    :param cwd: current working directory
88
    :param error: function to call for CLI errors
89
90
    """
91
    from doorstop import __project__, __version__
0 ignored issues
show
introduced by
Import outside toplevel (doorstop)
Loading history...
92
    # Exit if tkinter is not available
93
    if isinstance(tk, Mock) or isinstance(ttk, Mock):
94
        return error("tkinter is not available")
95
96
    else:  # pragma: no cover (manual test)
97
98
        root = tk.Tk()
99
        root.title("{} ({})".format(__project__, __version__))
100
        # TODO: set a minimum window size
101
        # root.minsize(1000, 600)
102
103
        # Start the application
104
        app = Application(root, cwd, args.project)
105
        app.mainloop()
106
107
        return True
108
109
110
def _log(func):  # pragma: no cover (manual test)
111
    """Decorator for methods that should log calls."""
112
    @functools.wraps(func)
113
    def wrapped(self, *args, **kwargs):
114
        """Wrapped method to log name and arguments."""
115
        sargs = "{}, {}".format(', '.join(repr(a) for a in args),
116
                                ', '.join("{}={}".format(k, repr(v))
117
                                          for k, v in kwargs.items()))
118
        msg = "{}: {}".format(func.__name__, sargs.strip(", "))
119
        if not self.ignore:
120
            log.critical(msg.strip())
121
        return func(self, *args, **kwargs)
122
    return wrapped
123
124
125 View Code Duplication
class Listbox2(tk.Listbox):  # pragma: no cover (manual test), pylint: disable=R0901
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
126
127
    """Listbox class with automatic width adjustment."""
128
129
    def autowidth(self, maxwidth=250):
130
        """Resize the widget width to fit contents."""
131
        fnt = font.Font(font=self.cget("font"))
132
        pixels = 0
133
        for item in self.get(0, "end"):
134
            pixels = max(pixels, fnt.measure(item))
135
        # bump listbox size until all entries fit
136
        pixels = pixels + 10
137
        width = int(self.cget("width"))
138
        for shift in range(0, maxwidth + 1, 5):
139
            if self.winfo_reqwidth() >= pixels:
140
                break
141
            self.config(width=width + shift)
142
143
144
class Application(ttk.Frame):  # pragma: no cover (manual test), pylint: disable=R0901,R0902
145
146
    """Graphical application for Doorstop."""
147
148 View Code Duplication
    def __init__(self, root, cwd, project):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
149
        ttk.Frame.__init__(self, root)
150
151
        # Create Doorstop variables
152
        self.cwd = cwd
153
        self.tree = None
154
        self.document = None
155
        self.item = None
156
        self.index = None
157
158
        # Create string variables
159
        self.stringvar_project = tk.StringVar(value=project or '')
160
        self.stringvar_project.trace('w', self.display_tree)
161
        self.stringvar_document = tk.StringVar()
162
        self.stringvar_document.trace('w', self.display_document)
163
        self.stringvar_item = tk.StringVar()
164
        self.stringvar_item.trace('w', self.display_item)
165
        self.stringvar_text = tk.StringVar()
166
        self.stringvar_text.trace('w', self.update_item)
167
        self.intvar_active = tk.IntVar()
168
        self.intvar_active.trace('w', self.update_item)
169
        self.intvar_derived = tk.IntVar()
170
        self.intvar_derived.trace('w', self.update_item)
171
        self.intvar_normative = tk.IntVar()
172
        self.intvar_normative.trace('w', self.update_item)
173
        self.intvar_heading = tk.IntVar()
174
        self.intvar_heading.trace('w', self.update_item)
175
        self.stringvar_link = tk.StringVar()  # no trace event
176
        self.stringvar_ref = tk.StringVar()
177
        self.stringvar_ref.trace('w', self.update_item)
178
        self.stringvar_extendedkey = tk.StringVar()
179
        self.stringvar_extendedkey.trace('w', self.display_extended)
180
        self.stringvar_extendedvalue = tk.StringVar()
181
        self.stringvar_extendedvalue.trace('w', self.update_item)
182
183
        # Create widget variables
184
        self.combobox_documents = None
185
        self.listbox_outline = None
186
        self.text_items = None
187
        self.text_item = None
188
        self.listbox_links = None
189
        self.combobox_extended = None
190
        self.text_extendedvalue = None
191
        self.text_parents = None
192
        self.text_children = None
193
194
        # Initialize the GUI
195
        self.ignore = False  # flag to ignore internal events
196
        frame = self.init(root)
197
        frame.pack(fill=tk.BOTH, expand=1)
198
199
        # Start the application
200
        root.after(500, self.find)
201
202
    def init(self, root):  # pylint: disable=R0912,R0914
203
        """Initialize and return the main frame."""  # pylint: disable=C0301
204
        # Shared arguments
205
        width_outline = 20
206
        width_text = 40
207
        width_code = 30
208
        width_uid = 10
209
        height_text = 10
210
        height_ext = 5
211
        height_code = 3
212
213
        # Shared keyword arguments
214
        kw_f = {'padding': 5}  # constructor arguments for frames
215
        kw_gp = {'padx': 2, 'pady': 2}  # grid arguments for padded widgets
216
        kw_gs = {'sticky': tk.NSEW}  # grid arguments for sticky widgets
217
        kw_gsp = dict(chain(kw_gs.items(), kw_gp.items()))  # grid arguments for sticky padded widgets
218
219
        # Shared style
220
        if sys.platform == 'darwin':
221
            size = 14
222
        else:
223
            size = 10
224
        normal = font.Font(family='TkDefaultFont', size=size)
225
        fixed = font.Font(family='Courier New', size=size)
226
227
        # Configure grid
228
        frame = ttk.Frame(root, **kw_f)
229
        frame.rowconfigure(0, weight=0)
230
        frame.rowconfigure(1, weight=1)
231
        frame.columnconfigure(0, weight=2)
232
        frame.columnconfigure(1, weight=1)
233
        frame.columnconfigure(2, weight=1)
234
        frame.columnconfigure(3, weight=2)
235
236
        # Create widgets
237
        def frame_project(root):
238
            """Frame for the current project."""
239
            # Configure grid
240
            frame = ttk.Frame(root, **kw_f)
241
            frame.rowconfigure(0, weight=1)
242
            frame.columnconfigure(0, weight=0)
243
            frame.columnconfigure(1, weight=1)
244
            frame.columnconfigure(2, weight=0)
245
246
            # Place widgets
247
            ttk.Label(frame, text="Project:").grid(row=0, column=0, **kw_gp)
248
            ttk.Entry(frame, textvariable=self.stringvar_project).grid(row=0, column=1, **kw_gsp)
249
            ttk.Button(frame, text="...", command=self.browse).grid(row=0, column=2, **kw_gp)
250
251
            return frame
252
253
        def frame_tree(root):
254
            """Frame for the current document."""
255
            # Configure grid
256
            frame = ttk.Frame(root, **kw_f)
257
            frame.rowconfigure(0, weight=1)
258
            frame.columnconfigure(0, weight=0)
259
            frame.columnconfigure(1, weight=1)
260
            frame.columnconfigure(2, weight=0)
261
262
            # Place widgets
263
            ttk.Label(frame, text="Document:").grid(row=0, column=0, **kw_gp)
264
            self.combobox_documents = ttk.Combobox(frame, textvariable=self.stringvar_document, state='readonly')
265
            self.combobox_documents.grid(row=0, column=1, **kw_gsp)
266
            ttk.Button(frame, text="New...", command=self.new).grid(row=0, column=2, **kw_gp)
267
268
            return frame
269
270
        def frame_document(root):
271
            """Frame for current document's outline and items."""
272
            # Configure grid
273
            frame = ttk.Frame(root, **kw_f)
274
            frame.rowconfigure(0, weight=0)
275
            frame.rowconfigure(1, weight=5)
276
            frame.rowconfigure(2, weight=0)
277
            frame.rowconfigure(3, weight=0)
278
            frame.rowconfigure(4, weight=1)
279
            frame.columnconfigure(0, weight=0)
280
            frame.columnconfigure(1, weight=0)
281
            frame.columnconfigure(2, weight=0)
282
            frame.columnconfigure(3, weight=0)
283
            frame.columnconfigure(4, weight=1)
284
            frame.columnconfigure(5, weight=1)
285
286
            def listbox_outline_listboxselect(event):
287
                """Callback for selecting an item."""
288
                widget = event.widget
289
                index = int(widget.curselection()[0])
290
                value = widget.get(index)
291
                self.stringvar_item.set(value)
292
293
            # Place widgets
294
            ttk.Label(frame, text="Outline:").grid(row=0, column=0, columnspan=4, sticky=tk.W, **kw_gp)
295
            ttk.Label(frame, text="Items:").grid(row=0, column=4, columnspan=2, sticky=tk.W, **kw_gp)
296
            self.listbox_outline = Listbox2(frame, width=width_outline, font=normal)
297
            self.listbox_outline.bind('<<ListboxSelect>>', listbox_outline_listboxselect)
298
            self.listbox_outline.grid(row=1, column=0, columnspan=4, **kw_gsp)
299
            self.text_items = tk.Text(frame, width=width_text, wrap=tk.WORD, font=normal)
300
            self.text_items.grid(row=1, column=4, columnspan=2, **kw_gsp)
301
            ttk.Button(frame, text="<", width=0, command=self.left).grid(row=2, column=0, sticky=tk.EW, padx=(2, 0))
302
            ttk.Button(frame, text="v", width=0, command=self.down).grid(row=2, column=1, sticky=tk.EW)
303
            ttk.Button(frame, text="^", width=0, command=self.up).grid(row=2, column=2, sticky=tk.EW)
304
            ttk.Button(frame, text=">", width=0, command=self.right).grid(row=2, column=3, sticky=tk.EW, padx=(0, 2))
305
            ttk.Button(frame, text="Add Item", command=self.add).grid(row=2, column=4, sticky=tk.W, **kw_gp)
306
            ttk.Button(frame, text="Remove Selected Item", command=self.remove).grid(row=2, column=5, sticky=tk.E, **kw_gp)
307
            ttk.Label(frame, text="Items Filter:").grid(row=3, column=0, columnspan=6, sticky=tk.W, **kw_gp)
308
            tk.Text(frame, height=height_code, width=width_code, wrap=tk.WORD, font=fixed).grid(row=4, column=0, columnspan=6, **kw_gsp)
309
310
            return frame
311
312
        def frame_item(root):
313
            """Frame for the currently selected item."""
314
            # Configure grid
315
            frame = ttk.Frame(root, **kw_f)
316
            frame.rowconfigure(0, weight=0)
317
            frame.rowconfigure(1, weight=4)
318
            frame.rowconfigure(2, weight=0)
319
            frame.rowconfigure(3, weight=1)
320
            frame.rowconfigure(4, weight=1)
321
            frame.rowconfigure(5, weight=1)
322
            frame.rowconfigure(6, weight=1)
323
            frame.rowconfigure(7, weight=0)
324
            frame.rowconfigure(8, weight=0)
325
            frame.rowconfigure(9, weight=0)
326
            frame.rowconfigure(10, weight=0)
327
            frame.rowconfigure(11, weight=4)
328
            frame.columnconfigure(0, weight=0, pad=kw_f['padding'] * 2)
329
            frame.columnconfigure(1, weight=1)
330
            frame.columnconfigure(2, weight=1)
331
332
            def text_item_focusout(event):
333
                """Callback for updating text."""
334
                widget = event.widget
335
                value = widget.get('1.0', 'end')
336
                self.stringvar_text.set(value)
337
338
            def text_extendedvalue_focusout(event):
339
                """Callback for updating extended attributes."""
340
                widget = event.widget
341
                value = widget.get('1.0', 'end')
342
                self.stringvar_extendedvalue.set(value)
343
344
            # Place widgets
345
            ttk.Label(frame, text="Selected Item:").grid(row=0, column=0, columnspan=3, sticky=tk.W, **kw_gp)
346
            self.text_item = tk.Text(frame, width=width_text, height=height_text, wrap=tk.WORD, font=fixed)
347
            self.text_item.bind('<FocusOut>', text_item_focusout)
348
            self.text_item.grid(row=1, column=0, columnspan=3, **kw_gsp)
349
            ttk.Label(frame, text="Properties:").grid(row=2, column=0, sticky=tk.W, **kw_gp)
350
            ttk.Label(frame, text="Links:").grid(row=2, column=1, columnspan=2, sticky=tk.W, **kw_gp)
351
            ttk.Checkbutton(frame, text="Active", variable=self.intvar_active).grid(row=3, column=0, sticky=tk.W, **kw_gp)
352
            self.listbox_links = tk.Listbox(frame, width=width_uid, height=6)
353
            self.listbox_links.grid(row=3, column=1, rowspan=4, **kw_gsp)
354
            ttk.Entry(frame, width=width_uid, textvariable=self.stringvar_link).grid(row=3, column=2, sticky=tk.EW + tk.N, **kw_gp)
355
            ttk.Checkbutton(frame, text="Derived", variable=self.intvar_derived).grid(row=4, column=0, sticky=tk.W, **kw_gp)
356
            ttk.Button(frame, text="<< Link Item", command=self.link).grid(row=4, column=2, **kw_gp)
357
            ttk.Checkbutton(frame, text="Normative", variable=self.intvar_normative).grid(row=5, column=0, sticky=tk.W, **kw_gp)
358
            ttk.Checkbutton(frame, text="Heading", variable=self.intvar_heading).grid(row=6, column=0, sticky=tk.W, **kw_gp)
359
            ttk.Button(frame, text=">> Unlink Item", command=self.unlink).grid(row=6, column=2, **kw_gp)
360
            ttk.Label(frame, text="External Reference:").grid(row=7, column=0, columnspan=3, sticky=tk.W, **kw_gp)
361
            ttk.Entry(frame, width=width_text, textvariable=self.stringvar_ref, font=fixed).grid(row=8, column=0, columnspan=3, **kw_gsp)
362
            ttk.Label(frame, text="Extended Attributes:").grid(row=9, column=0, columnspan=3, sticky=tk.W, **kw_gp)
363
            self.combobox_extended = ttk.Combobox(frame, textvariable=self.stringvar_extendedkey, font=fixed)
364
            self.combobox_extended.grid(row=10, column=0, columnspan=3, **kw_gsp)
365
            self.text_extendedvalue = tk.Text(frame, width=width_text, height=height_ext, wrap=tk.WORD, font=fixed)
366
            self.text_extendedvalue.bind('<FocusOut>', text_extendedvalue_focusout)
367
            self.text_extendedvalue.grid(row=11, column=0, columnspan=3, **kw_gsp)
368
369
            return frame
370
371
        def frame_family(root):
372
            """Frame for the parent and child document items."""
373
            # Configure grid
374
            frame = ttk.Frame(root, **kw_f)
375
            frame.rowconfigure(0, weight=0)
376
            frame.rowconfigure(1, weight=1)
377
            frame.rowconfigure(2, weight=0)
378
            frame.rowconfigure(3, weight=1)
379
            frame.columnconfigure(0, weight=1)
380
381
            # Place widgets
382
            ttk.Label(frame, text="Linked To:").grid(row=0, column=0, sticky=tk.W, **kw_gp)
383
            self.text_parents = tk.Text(frame, width=width_text, wrap=tk.WORD, font=normal)
384
            self.text_parents.grid(row=1, column=0, **kw_gsp)
385
            ttk.Label(frame, text="Linked From:").grid(row=2, column=0, sticky=tk.W, **kw_gp)
386
            self.text_children = tk.Text(frame, width=width_text, wrap=tk.WORD, font=normal)
387
            self.text_children.grid(row=3, column=0, **kw_gsp)
388
389
            return frame
390
391
        # Place widgets
392
        frame_project(frame).grid(row=0, column=0, columnspan=2, **kw_gs)
393
        frame_tree(frame).grid(row=0, column=2, columnspan=2, **kw_gs)
394
        frame_document(frame).grid(row=1, column=0, **kw_gs)
395
        frame_item(frame).grid(row=1, column=1, columnspan=2, **kw_gs)
396
        frame_family(frame).grid(row=1, column=3, **kw_gs)
397
398
        return frame
399
400
    def find(self):
401
        """Find the root of the project."""
402
        if not self.stringvar_project.get():
403
            try:
404
                path = vcs.find_root(self.cwd)
405
            except DoorstopError as exc:
406
                log.error(exc)
407
            else:
408
                self.stringvar_project.set(path)
409
410
    @_log
411
    def browse(self):
412
        """Browse for the root of a project."""
413
        path = filedialog.askdirectory()
414
        log.debug("path: {}".format(path))
415
        if path:
416
            self.stringvar_project.set(path)
417
418
    def display_tree(self, *_):
419
        """Display the currently selected tree."""
420
        # Set the current tree
421
        self.tree = builder.build(root=self.stringvar_project.get())
422
        log.info("displaying tree...")
423
424
        # Display the documents in the tree
425
        values = ["{} ({})".format(document.prefix, document.relpath)
426
                  for document in self.tree]
427
        self.combobox_documents['values'] = values
428
429
        # Select the first document
430
        self.combobox_documents.current(0)
431
432
    def display_document(self, *_):
433
        """Display the currently selected document."""
434
        # Set the current document
435
        index = self.combobox_documents.current()
436
        self.document = list(self.tree)[index]
437
        log.info("displaying document {}...".format(self.document))
438
439
        # Display the items in the document
440
        self.listbox_outline.delete(0, tk.END)
441
        self.text_items.delete('1.0', 'end')
442
        for item in self.document.items:
443
444
            # Add the item to the document outline
445
            indent = '  ' * (item.depth - 1)
446
            level = '.'.join(str(l) for l in item.level)
447
            value = "{s}{l} {u}".format(s=indent, l=level, u=item.uid)
448
            self.listbox_outline.insert(tk.END, value)
449
450
            # Add the item to the document text
451
            value = "{t} [{u}]\n\n".format(t=item.text or item.ref or '???',
452
                                           u=item.uid)
453
            self.text_items.insert('end', value)
454
        self.listbox_outline.autowidth()
455
456
        # Select the first item
457
        self.listbox_outline.selection_set(self.index or 0)
458
        uid = self.listbox_outline.selection_get()
459
        self.stringvar_item.set(uid)  # manual call
460
461
    def display_item(self, *_):
462
        """Display the currently selected item."""
463
        self.ignore = True
464
465
        # Set the current item
466
        uid = self.stringvar_item.get().rsplit(' ', 1)[-1]
467
        self.item = self.tree.find_item(uid)
468
        self.index = self.listbox_outline.curselection()[0]
469
        log.info("displaying item {}...".format(self.item))
470
471
        # Display the item's text
472
        self.text_item.replace('1.0', 'end', self.item.text)
473
474
        # Display the item's properties
475
        self.stringvar_text.set(self.item.text)  # manual call
476
        self.intvar_active.set(self.item.active)
477
        self.intvar_derived.set(self.item.derived)
478
        self.intvar_normative.set(self.item.normative)
479
        self.intvar_heading.set(self.item.heading)
480
481
        # Display the item's links
482
        self.listbox_links.delete(0, tk.END)
483
        for uid in self.item.links:
484
            self.listbox_links.insert(tk.END, uid)
485
        self.stringvar_link.set('')
486
487
        # Display the item's external reference
488
        self.stringvar_ref.set(self.item.ref)
489
490
        # Display the item's extended attributes
491
        values = self.item.extended
492
        self.combobox_extended['values'] = values or ['']
493
        self.combobox_extended.current(0)
494
495
        # Display the items this item links to
496
        self.text_parents.delete('1.0', 'end')
497
        for uid in self.item.links:
498
            try:
499
                item = self.tree.find_item(uid)
500
            except DoorstopError:
501
                text = "???"
502
            else:
503
                text = item.text or item.ref or '???'
504
                uid = item.uid
505
            chars = "{t} [{u}]\n\n".format(t=text, u=uid)
506
            self.text_parents.insert('end', chars)
507
508
        # Display the items this item has links from
509
        self.text_children.delete('1.0', 'end')
510
        identifiers = self.item.find_child_links()
511
        for uid in identifiers:
512
            item = self.tree.find_item(uid)
513
            text = item.text or item.ref or '???'
514
            uid = item.uid
515
            chars = "{t} [{u}]\n\n".format(t=text, u=uid)
516
            self.text_children.insert('end', chars)
517
518
        self.ignore = False
519
520
    @_log
521
    def display_extended(self, *_):
522
        """Display the currently selected extended attribute."""
523
        self.ignore = True
524
525
        name = self.stringvar_extendedkey.get()
526
        log.debug("displaying extended attribute '{}'...".format(name))
527
        self.text_extendedvalue.replace('1.0', 'end', self.item.get(name, ''))
528
529
        self.ignore = False
530
531 View Code Duplication
    def update_item(self, *_):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
532
        """Update the current item from the fields."""
533
        if self.ignore:
534
            return
535
536
        # Update the current item
537
        log.info("updating {}...".format(self.item))
538
        self.item.auto = False
539
        self.item.text = self.stringvar_text.get()
540
        self.item.active = self.intvar_active.get()
541
        self.item.derived = self.intvar_derived.get()
542
        self.item.normative = self.intvar_normative.get()
543
        self.item.heading = self.intvar_heading.get()
544
        self.item.links = self.listbox_links.get(0, tk.END)
545
        self.item.ref = self.stringvar_ref.get()
546
        name = self.stringvar_extendedkey.get()
547
        if name:
548
            self.item.set(name, self.stringvar_extendedvalue.get())
549
        self.item.save()
550
551
        # Re-select this item
552
        self.display_document()
553
554
    @_log
555
    def new(self):
556
        """Create a new document."""
557
558
    @_log
559
    def left(self):
560
        """Dedent the current item's level."""
561
562
    @_log
563
    def down(self):
564
        """Increment the current item's level."""
565
566
    @_log
567
    def up(self):
568
        """Decrement the current item's level."""
569
570
    @_log
571
    def right(self):
572
        """Indent the current item's level."""
573
574
    @_log
575
    def add(self):
576
        """Add a new item to the document."""
577
578
    @_log
579
    def remove(self):
580
        """Remove the selected item from the document."""
581
582
    def link(self):
583
        """Add the specified link to the current item."""
584
        # Add the specified link to the list
585
        uid = self.stringvar_link.get()
586
        if uid:
587
            self.listbox_links.insert(tk.END, uid)
588
            self.stringvar_link.set('')
589
590
            # Update the current item
591
            self.update_item()
592
593
    def unlink(self):
594
        """Remove the currently selected link from the current item."""
595
        # Remove the selected link from the list
596
        index = self.listbox_links.curselection()
597
        self.listbox_links.delete(index)
598
599
        # Update the current item
600
        self.update_item()
601
602
603
if __name__ == '__main__':  # pragma: no cover (manual test)
604
    main()
605