Passed
Push — develop ( 26c92a...4c3cbb )
by Jace
01:06
created

Application.frame_document()   B

Complexity

Conditions 4

Size

Total Lines 43

Duplication

Lines 1
Ratio 2.33 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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