doorstop.gui.application   F
last analyzed

Complexity

Total Complexity 78

Size/Duplication

Total Lines 850
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 78
eloc 573
dl 0
loc 850
rs 2.16
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A Application.add() 0 14 2
C Application.init() 0 284 8
A Application.create_properties_widget() 0 25 1
A Application.display_tree() 0 19 2
A Application.remove() 0 23 4
A Application.followlink() 0 15 1
A Application.update_item() 0 27 4
A Application.link() 0 11 2
A Application.up() 0 6 1
A Application.unlink() 0 11 2
A Application.create_links_widget() 0 36 1
C Application.display_document() 0 80 10
A Application.create_reference_widget() 0 15 1
A Application.right() 0 6 1
A Application.left() 0 6 1
F Application.display_item() 0 110 25
A Application.display_extended() 0 11 1
A Application.find() 0 10 4
A Application.browse() 0 7 2
B Application.__init__() 0 58 1
A Application.down() 0 6 1

1 Function

Rating   Name   Duplication   Size   Complexity  
A _log() 0 15 3

How to fix   Complexity   

Complexity

Complex classes like doorstop.gui.application often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python
2
# SPDX-License-Identifier: LGPL-3.0-only
3
# type: ignore
4
5
"""Graphical interface for Doorstop."""
6
7
import functools
8
import logging
9
import sys
10
from itertools import chain
11
from unittest.mock import Mock
12
13
from doorstop import common
14
from doorstop.common import DoorstopError
15
from doorstop.core import builder, vcs
16
from doorstop.gui import utilTkinter, widget
17
18
try:
19
    import tkinter as tk
20
    from tkinter import filedialog, ttk
21
except ImportError as _exc:
22
    sys.stderr.write("WARNING: {}\n".format(_exc))
23
    tk = Mock()
24
    ttk = Mock()
25
26
27
log = common.logger(__name__)
28
29
30
def _log(func):
31
    """Log name and arguments."""
32
33
    @functools.wraps(func)
34
    def wrapped(self, *args, **kwargs):
35
        sargs = "{}, {}".format(
36
            ", ".join(repr(a) for a in args),
37
            ", ".join("{}={}".format(k, repr(v)) for k, v in kwargs.items()),
38
        )
39
        msg = "log: {}: {}".format(func.__name__, sargs.strip(", "))
40
        if not isinstance(self, ttk.Frame) or not self.ignore:
41
            log.debug(msg.strip())
42
        return func(self, *args, **kwargs)
43
44
    return wrapped
45
46
47
class Application(ttk.Frame):
48
    """Graphical application for Doorstop."""
49
50
    def __init__(self, root, cwd, project):
51
        ttk.Frame.__init__(self, root)
52
53
        # Create Doorstop variables
54
        self.cwd = cwd
55
        self.tree = None
56
        self.document = None
57
        self.item = None
58
59
        # Create string variables
60
        self.stringvar_project = tk.StringVar(value=project or "")
61
        self.stringvar_project.trace("w", self.display_tree)
62
        self.stringvar_document = tk.StringVar()
63
        self.stringvar_document.trace("w", self.display_document)
64
65
        # The stringvar_item holds the uid of the main selected item (or empty string if nothing is selected).
66
        self.stringvar_item = tk.StringVar()
67
        self.stringvar_item.trace("w", self.display_item)
68
69
        # The item header
70
        self.stringvar_header = tk.StringVar()
71
        self.stringvar_header.trace("w", self.update_item)
72
73
        self.stringvar_text = tk.StringVar()
74
        self.stringvar_text.trace("w", self.update_item)
75
        self.intvar_active = tk.IntVar()
76
        self.intvar_active.trace("w", self.update_item)
77
        self.intvar_derived = tk.IntVar()
78
        self.intvar_derived.trace("w", self.update_item)
79
        self.intvar_normative = tk.IntVar()
80
        self.intvar_normative.trace("w", self.update_item)
81
        self.intvar_heading = tk.IntVar()
82
        self.intvar_heading.trace("w", self.update_item)
83
        self.stringvar_link = tk.StringVar()  # no trace event
84
        self.stringvar_ref = tk.StringVar()
85
        self.stringvar_ref.trace("w", self.update_item)
86
        self.stringvar_extendedkey = tk.StringVar()
87
        self.stringvar_extendedkey.trace("w", self.display_extended)
88
        self.stringvar_extendedvalue = tk.StringVar()
89
        self.stringvar_extendedvalue.trace("w", self.update_item)
90
91
        # Create widget variables
92
        self.combobox_documents = None
93
        self.text_items = None
94
        self.text_item = None
95
        self.listbox_links = None
96
        self.combobox_extended = None
97
        self.text_extendedvalue = None
98
        self.text_parents = None
99
        self.text_children = None
100
101
        # Initialize the GUI
102
        self.ignore = False  # flag to ignore internal events
103
        frame = self.init(root)
104
        frame.pack(fill=tk.BOTH, expand=1)
105
106
        # Start the application
107
        root.after(500, self.find)
108
109
    def init(self, root):
110
        """Initialize and return the main frame."""
111
        # pylint: disable=attribute-defined-outside-init
112
113
        # Shared arguments
114
        width_text = 30
115
        height_text = 10
116
        height_ext = 5
117
118
        # Shared keyword arguments
119
        kw_f = {"padding": 5}  # constructor arguments for frames
120
        kw_gp = {"padx": 2, "pady": 2}  # grid arguments for padded widgets
121
        kw_gs = {"sticky": tk.NSEW}  # grid arguments for sticky widgets
122
        kw_gsp = dict(
123
            chain(kw_gs.items(), kw_gp.items())
124
        )  # grid arguments for sticky padded widgets
125
126
        root.bind_all("<Control-minus>", lambda arg: widget.adjustFontSize(-1))
127
        root.bind_all("<Control-equal>", lambda arg: widget.adjustFontSize(1))
128
        root.bind_all("<Control-0>", lambda arg: widget.resetFontSize())
129
130
        # Configure grid
131
        frame = ttk.Frame(root, **kw_f)
132
        frame.rowconfigure(0, weight=0)
133
        frame.rowconfigure(1, weight=1)
134
        frame.columnconfigure(0, weight=2)
135
        frame.columnconfigure(1, weight=1)
136
        frame.columnconfigure(2, weight=1)
137
        frame.columnconfigure(3, weight=2)
138
139
        # Create widgets
140
        def frame_project(root):
141
            """Frame for the current project."""
142
            # Configure grid
143
            frame = ttk.Frame(root, **kw_f)
144
            frame.rowconfigure(0, weight=1)
145
            frame.columnconfigure(0, weight=0)
146
            frame.columnconfigure(1, weight=1)
147
148
            # Place widgets
149
            widget.Label(frame, text="Project:").grid(row=0, column=0, **kw_gp)
150
            widget.Entry(frame, textvariable=self.stringvar_project).grid(
151
                row=0, column=1, **kw_gsp
152
            )
153
154
            return frame
155
156
        def frame_tree(root):
157
            """Frame for the current document."""
158
            # Configure grid
159
            frame = ttk.Frame(root, **kw_f)
160
            frame.rowconfigure(0, weight=1)
161
            frame.columnconfigure(0, weight=0)
162
            frame.columnconfigure(1, weight=1)
163
164
            # Place widgets
165
            widget.Label(frame, text="Document:").grid(row=0, column=0, **kw_gp)
166
            self.combobox_documents = widget.Combobox(
167
                frame, textvariable=self.stringvar_document, state="readonly"
168
            )
169
            self.combobox_documents.grid(row=0, column=1, **kw_gsp)
170
171
            return frame
172
173
        def frame_document(root):
174
            """Frame for current document's outline and items."""
175
            # Configure grid
176
            frame = ttk.Frame(root, **kw_f)
177
            frame.rowconfigure(0, weight=0)
178
            frame.rowconfigure(1, weight=5)
179
            frame.rowconfigure(2, weight=0)
180
            frame.rowconfigure(3, weight=0)
181
            frame.columnconfigure(0, weight=0)
182
            frame.columnconfigure(1, weight=0)
183
            frame.columnconfigure(2, weight=0)
184
            frame.columnconfigure(3, weight=0)
185
            frame.columnconfigure(4, weight=1)
186
            frame.columnconfigure(5, weight=1)
187
188
            @_log
189
            def treeview_outline_treeviewselect(event):
190
                """Handle selecting an item in the tree view."""
191
                if self.ignore:
192
                    return
193
                thewidget = event.widget
194
                curselection = thewidget.selection()
195
                if curselection:
196
                    uid = curselection[0]
197
                    self.stringvar_item.set(uid)
198
199
            @_log
200
            def treeview_outline_delete(event):  # pylint: disable=W0613
201
                """Handle deleting an item in the tree view."""
202
                if self.ignore:
203
                    return
204
                self.remove()
205
206
            # Place widgets
207
            widget.Label(frame, text="Outline:").grid(
208
                row=0, column=0, columnspan=4, sticky=tk.W, **kw_gp
209
            )
210
            widget.Label(frame, text="Items:").grid(
211
                row=0, column=4, columnspan=2, sticky=tk.W, **kw_gp
212
            )
213
            c_columnId = ("Id",)
214
            self.treeview_outline = widget.TreeView(frame, columns=c_columnId)
215
            for col in c_columnId:
216
                self.treeview_outline.heading(col, text=col)
217
218
            # Add a Vertical scrollbar to the Treeview Outline
219
            treeview_outline_verticalScrollBar = widget.ScrollbarV(
220
                frame, command=self.treeview_outline.yview
221
            )
222
            treeview_outline_verticalScrollBar.grid(
223
                row=1, column=0, columnspan=1, **kw_gs
224
            )
225
            self.treeview_outline.configure(
226
                yscrollcommand=treeview_outline_verticalScrollBar.set
227
            )
228
229
            self.treeview_outline.bind(
230
                "<<TreeviewSelect>>", treeview_outline_treeviewselect
231
            )
232
            self.treeview_outline.bind("<Delete>", treeview_outline_delete)
233
            self.treeview_outline.grid(row=1, column=1, columnspan=3, **kw_gsp)
234
            self.text_items = widget.noUserInput_init(
235
                widget.Text(frame, width=width_text, wrap=tk.WORD)
236
            )
237
            self.text_items.grid(row=1, column=4, columnspan=2, **kw_gsp)
238
            self.text_items_hyperlink = utilTkinter.HyperlinkManager(self.text_items)
239
            widget.Button(frame, text="<", width=0, command=self.left).grid(
240
                row=2, column=0, sticky=tk.EW, padx=(2, 0)
241
            )
242
            widget.Button(frame, text="v", width=0, command=self.down).grid(
243
                row=2, column=1, sticky=tk.EW
244
            )
245
            widget.Button(frame, text="^", width=0, command=self.up).grid(
246
                row=2, column=2, sticky=tk.EW
247
            )
248
            widget.Button(frame, text=">", width=0, command=self.right).grid(
249
                row=2, column=3, sticky=tk.EW, padx=(0, 2)
250
            )
251
            widget.Button(frame, text="Add Item", command=self.add).grid(
252
                row=2, column=4, sticky=tk.W, **kw_gp
253
            )
254
            widget.Button(frame, text="Remove Selected Item", command=self.remove).grid(
255
                row=2, column=5, sticky=tk.E, **kw_gp
256
            )
257
258
            return frame
259
260
        def frame_item(root):
261
            """Frame for the currently selected item."""
262
            # Configure grid
263
            frame = ttk.Frame(root, **kw_f)
264
            frame.rowconfigure(0, weight=0)
265
            frame.rowconfigure(1, weight=0)
266
            frame.rowconfigure(2, weight=0)
267
            frame.rowconfigure(3, weight=4)
268
            frame.rowconfigure(4, weight=1)
269
            frame.rowconfigure(5, weight=1)
270
            frame.rowconfigure(6, weight=1)
271
            frame.rowconfigure(7, weight=0)
272
            frame.rowconfigure(8, weight=0)
273
            frame.rowconfigure(9, weight=0)
274
            frame.rowconfigure(10, weight=0)
275
            frame.rowconfigure(11, weight=0)
276
            frame.rowconfigure(12, weight=0)
277
            frame.rowconfigure(13, weight=4)
278
            frame.columnconfigure(0, weight=1, pad=kw_f["padding"] * 2)
279
            frame.columnconfigure(1, weight=1)
280
281
            @_log
282
            def text_focusin(_):
283
                """Handle entering a text field."""
284
                self.ignore = True
285
286
            @_log
287
            def text_item_focusout(event):
288
                """Handle updated text text."""
289
                self.ignore = False
290
                thewidget = event.widget
291
                value = thewidget.get("1.0", tk.END)
292
                self.stringvar_text.set(value)
293
294
            @_log
295
            def text_extendedvalue_focusout(event):
296
                """Handle updated extended attributes."""
297
                self.ignore = False
298
                thewidget = event.widget
299
                value = thewidget.get("1.0", tk.END)
300
                self.stringvar_extendedvalue.set(value)
301
302
            # Item header
303
            widget.Label(frame, text="Header:").grid(
304
                row=0, column=0, columnspan=3, sticky=tk.W, **kw_gp
305
            )
306
            self.text_header = widget.Entry(frame, textvariable=self.stringvar_header)
307
            self.text_header.grid(row=1, column=0, columnspan=3, **kw_gsp)
308
309
            # Selected Item
310
            widget.Label(frame, text="Selected Item:").grid(
311
                row=2, column=0, columnspan=3, sticky=tk.W, **kw_gp
312
            )
313
            self.text_item = widget.Text(
314
                frame, width=width_text, height=height_text, wrap=tk.WORD
315
            )
316
            self.text_item.bind("<FocusIn>", text_focusin)
317
            self.text_item.bind("<FocusOut>", text_item_focusout)
318
            self.text_item.grid(row=3, column=0, columnspan=3, **kw_gsp)
319
320
            # Column: Properties
321
            self.create_properties_widget(frame).grid(
322
                row=4, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
323
            )
324
325
            # Column: Links
326
            self.create_links_widget(frame).grid(
327
                row=6, rowspan=3, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
328
            )
329
330
            # External Reference
331
            self.create_reference_widget(frame).grid(
332
                row=9, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
333
            )
334
335
            widget.Label(frame, text="Extended Attributes:").grid(
336
                row=11, column=0, columnspan=3, sticky=tk.W, **kw_gp
337
            )
338
            self.combobox_extended = widget.Combobox(
339
                frame, textvariable=self.stringvar_extendedkey
340
            )
341
            self.combobox_extended.grid(row=12, column=0, columnspan=3, **kw_gsp)
342
            self.text_extendedvalue = widget.Text(
343
                frame, width=width_text, height=height_ext, wrap=tk.WORD
344
            )
345
            self.text_extendedvalue.bind("<FocusIn>", text_focusin)
346
            self.text_extendedvalue.bind("<FocusOut>", text_extendedvalue_focusout)
347
            self.text_extendedvalue.grid(row=13, column=0, columnspan=3, **kw_gsp)
348
349
            return frame
350
351
        def frame_family(root):
352
            """Frame for the parent and child document items."""
353
            # Configure grid
354
            frame = ttk.Frame(root, **kw_f)
355
            frame.rowconfigure(0, weight=0)
356
            frame.rowconfigure(1, weight=1)
357
            frame.rowconfigure(2, weight=0)
358
            frame.rowconfigure(3, weight=1)
359
            frame.columnconfigure(0, weight=1)
360
361
            # Place widgets
362
            widget.Label(frame, text="Linked To:").grid(
363
                row=0, column=0, sticky=tk.W, **kw_gp
364
            )
365
            self.text_parents = widget.noUserInput_init(
366
                widget.Text(frame, width=width_text, wrap=tk.WORD)
367
            )
368
            self.text_parents_hyperlink = utilTkinter.HyperlinkManager(
369
                self.text_parents
370
            )
371
            self.text_parents.grid(row=1, column=0, **kw_gsp)
372
            widget.Label(frame, text="Linked From:").grid(
373
                row=2, column=0, sticky=tk.W, **kw_gp
374
            )
375
            self.text_children = widget.noUserInput_init(
376
                widget.Text(frame, width=width_text, wrap=tk.WORD)
377
            )
378
            self.text_children_hyperlink = utilTkinter.HyperlinkManager(
379
                self.text_children
380
            )
381
            self.text_children.grid(row=3, column=0, **kw_gsp)
382
383
            return frame
384
385
        # Place widgets
386
        frame_project(frame).grid(row=0, column=0, columnspan=2, **kw_gs)
387
        frame_tree(frame).grid(row=0, column=2, columnspan=2, **kw_gs)
388
        frame_document(frame).grid(row=1, column=0, **kw_gs)
389
        frame_item(frame).grid(row=1, column=1, columnspan=2, **kw_gs)
390
        frame_family(frame).grid(row=1, column=3, **kw_gs)
391
392
        return frame
393
394
    @_log
395
    def find(self):
396
        """Find the root of the project."""
397
        if not self.stringvar_project.get():
398
            try:
399
                path = vcs.find_root(self.cwd)
400
            except DoorstopError as exc:
401
                log.error(exc)
402
            else:
403
                self.stringvar_project.set(path)
404
405
    @_log
406
    def browse(self):
407
        """Browse for the root of a project."""
408
        path = filedialog.askdirectory()
409
        log.debug("path: {}".format(path))
410
        if path:
411
            self.stringvar_project.set(path)
412
413
    @_log
414
    def display_tree(self, *_):
415
        """Display the currently selected tree."""
416
        # Set the current tree
417
        self.tree = builder.build(root=self.stringvar_project.get())
418
        log.info("displaying tree...")
419
420
        # Display the documents in the tree
421
        values = [
422
            "{} ({})".format(document.prefix, document.relpath)
423
            for document in self.tree
424
        ]
425
        self.combobox_documents["values"] = values
426
427
        # Select the first document
428
        if len(self.tree):  # pylint: disable=len-as-condition
429
            self.combobox_documents.current(0)
430
        else:
431
            logging.warning("no documents to display")
432
433
    @_log
434
    def display_document(self, *_):
435
        """Display the currently selected document."""
436
        # Set the current document
437
        index = self.combobox_documents.current()
438
        self.document = list(self.tree)[index]
439
        log.info("displaying document {}...".format(self.document))
440
441
        # Record the currently opened items.
442
        c_openItem = []
443
        for c_currUID in utilTkinter.getAllChildren(self.treeview_outline):
444
            if self.treeview_outline.item(c_currUID)["open"]:
445
                c_openItem.append(c_currUID)
446
447
        # Record the currently selected items.
448
        c_selectedItem = self.treeview_outline.selection()
449
450
        # Clear the widgets
451
        self.treeview_outline.delete(*self.treeview_outline.get_children())
452
        widget.noUserInput_delete(self.text_items, "1.0", tk.END)
453
        self.text_items_hyperlink.reset()
454
455
        # Display the items in the document
456
        c_levelsItem = [""]
457
        for item in self.document.items:
458
            theParent = next(
459
                iter(reversed([x for x in c_levelsItem[: item.depth]])), ""
460
            )
461
462
            while len(c_levelsItem) < item.depth:
463
                c_levelsItem.append(item.uid)
464
            c_levelsItem = c_levelsItem[: item.depth]
465
            for x in range(item.depth):
466
                c_levelsItem.append(item.uid)
467
468
            # Add the item to the document outline
469
            self.treeview_outline.insert(
470
                theParent,
471
                tk.END,
472
                item.uid,
473
                text=item.level,
474
                values=(item.uid,),
475
                open=item.uid in c_openItem,
476
            )
477
478
            # Add the item to the document text
479
            widget.noUserInput_insert(
480
                self.text_items, tk.END, "{t}".format(t=item.text or item.ref or "???")
481
            )
482
            widget.noUserInput_insert(self.text_items, tk.END, " [")
483
            widget.noUserInput_insert(
484
                self.text_items,
485
                tk.END,
486
                item.uid,
487
                self.text_items_hyperlink.add(
488
                    # pylint: disable=unnecessary-lambda
489
                    lambda c_theURL: self.followlink(c_theURL),
490
                    item.uid,
491
                    ["refLink"],
492
                ),
493
            )
494
            widget.noUserInput_insert(self.text_items, tk.END, "]\n\n")
495
496
        # Set tree view selection
497
        c_selectedItem = [
498
            x
499
            for x in c_selectedItem
500
            if x in utilTkinter.getAllChildren(self.treeview_outline)
501
        ]
502
        if c_selectedItem:
503
            # Restore selection
504
            self.treeview_outline.selection_set(c_selectedItem)
505
        else:
506
            # Select the first item
507
            for uid in utilTkinter.getAllChildren(self.treeview_outline):
508
                self.stringvar_item.set(uid)
509
                break
510
            else:
511
                logging.warning("no items to display")
512
                self.stringvar_item.set("")
513
514
    @_log
515
    def display_item(self, *_):
516
        """Display the currently selected item."""
517
        try:
518
            self.ignore = True
519
520
            # Fetch the current item
521
            uid = self.stringvar_item.get()
522
            if uid == "":
523
                self.item = None
524
            else:
525
                try:
526
                    self.item = self.tree.find_item(uid)
527
                except DoorstopError:
528
                    pass
529
            log.info("displaying item {}...".format(self.item))
530
531
            if uid != "":
532
                if uid not in self.treeview_outline.selection():
533
                    self.treeview_outline.selection_set((uid,))
534
                self.treeview_outline.see(uid)
535
536
            # Display the item's text
537
            self.text_item.replace(
538
                "1.0", tk.END, "" if self.item is None else self.item.text
539
            )
540
541
            # Display the item's properties
542
            self.stringvar_header.set("" if self.item is None else self.item.header)
543
            self.stringvar_text.set("" if self.item is None else self.item.text)
544
            self.intvar_active.set(False if self.item is None else self.item.active)
545
            self.intvar_derived.set(False if self.item is None else self.item.derived)
546
            self.intvar_normative.set(
547
                False if self.item is None else self.item.normative
548
            )
549
            self.intvar_heading.set(False if self.item is None else self.item.heading)
550
551
            # Display the item's links
552
            self.listbox_links.delete(0, tk.END)
553
            if self.item is not None:
554
                for uid in self.item.links:
555
                    self.listbox_links.insert(tk.END, uid)
556
            self.stringvar_link.set("")
557
558
            # Display the item's external reference
559
            self.stringvar_ref.set("" if self.item is None else self.item.ref)
560
561
            # Display the item's extended attributes
562
            values = None if self.item is None else self.item.extended
563
            self.combobox_extended["values"] = values or [""]
564
            if self.item is not None:
565
                self.combobox_extended.current(0)
566
567
            # Display the items this item links to
568
            widget.noUserInput_delete(self.text_parents, "1.0", tk.END)
569
            self.text_parents_hyperlink.reset()
570
            if self.item is not None:
571
                for uid in self.item.links:
572
                    try:
573
                        item = self.tree.find_item(uid)
574
                    except DoorstopError:
575
                        text = "???"
576
                    else:
577
                        text = item.text or item.ref or "???"
578
                        uid = item.uid
579
580
                    widget.noUserInput_insert(
581
                        self.text_parents, tk.END, "{t}".format(t=text)
582
                    )
583
                    widget.noUserInput_insert(self.text_parents, tk.END, " [")
584
                    widget.noUserInput_insert(
585
                        self.text_parents,
586
                        tk.END,
587
                        uid,
588
                        self.text_parents_hyperlink.add(
589
                            # pylint: disable=unnecessary-lambda
590
                            lambda c_theURL: self.followlink(c_theURL),
591
                            uid,
592
                            ["refLink"],
593
                        ),
594
                    )
595
                    widget.noUserInput_insert(self.text_parents, tk.END, "]\n\n")
596
597
            # Display the items this item has links from
598
            widget.noUserInput_delete(self.text_children, "1.0", "end")
599
            self.text_children_hyperlink.reset()
600
            if self.item is not None:
601
                for uid in self.item.find_child_links():
602
                    item = self.tree.find_item(uid)
603
                    text = item.text or item.ref or "???"
604
                    uid = item.uid
605
606
                    widget.noUserInput_insert(
607
                        self.text_children, tk.END, "{t}".format(t=text)
608
                    )
609
                    widget.noUserInput_insert(self.text_children, tk.END, " [")
610
                    widget.noUserInput_insert(
611
                        self.text_children,
612
                        tk.END,
613
                        uid,
614
                        self.text_children_hyperlink.add(
615
                            # pylint: disable=unnecessary-lambda
616
                            lambda c_theURL: self.followlink(c_theURL),
617
                            uid,
618
                            ["refLink"],
619
                        ),
620
                    )
621
                    widget.noUserInput_insert(self.text_children, tk.END, "]\n\n")
622
        finally:
623
            self.ignore = False
624
625
    @_log
626
    def display_extended(self, *_):
627
        """Display the currently selected extended attribute."""
628
        try:
629
            self.ignore = True
630
631
            name = self.stringvar_extendedkey.get()
632
            log.debug("displaying extended attribute '{}'...".format(name))
633
            self.text_extendedvalue.replace("1.0", tk.END, self.item.get(name, ""))
634
        finally:
635
            self.ignore = False
636
637
    @_log
638
    def update_item(self, *_):
639
        """Update the current item from the fields."""
640
        if self.ignore:
641
            return
642
        if not self.item:
643
            logging.warning("no item selected")
644
            return
645
646
        # Update the current item
647
        log.info("updating {}...".format(self.item))
648
        self.item.auto = False
649
        self.item.header = self.stringvar_header.get()
650
        self.item.text = self.stringvar_text.get()
651
        self.item.active = self.intvar_active.get()
652
        self.item.derived = self.intvar_derived.get()
653
        self.item.normative = self.intvar_normative.get()
654
        self.item.heading = self.intvar_heading.get()
655
        self.item.links = self.listbox_links.get(0, tk.END)
656
        self.item.ref = self.stringvar_ref.get()
657
        name = self.stringvar_extendedkey.get()
658
        if name:
659
            self.item.set(name, self.stringvar_extendedvalue.get())
660
        self.item.save()
661
662
        # Re-select this item
663
        self.display_document()
664
665
    @_log
666
    def left(self):
667
        """Dedent the current item's level."""
668
        self.item.level <<= 1
669
        self.document.reorder(keep=self.item)
670
        self.display_document()
671
672
    @_log
673
    def down(self):
674
        """Increment the current item's level."""
675
        self.item.level += 1
676
        self.document.reorder(keep=self.item)
677
        self.display_document()
678
679
    @_log
680
    def up(self):
681
        """Decrement the current item's level."""
682
        self.item.level -= 1
683
        self.document.reorder(keep=self.item)
684
        self.display_document()
685
686
    @_log
687
    def right(self):
688
        """Indent the current item's level."""
689
        self.item.level >>= 1
690
        self.document.reorder(keep=self.item)
691
        self.display_document()
692
693
    @_log
694
    def add(self):
695
        """Add a new item to the document."""
696
        logging.info("adding item to {}...".format(self.document))
697
        if self.item:
698
            level = self.item.level + 1
699
        else:
700
            level = None
701
        item = self.document.add_item(level=level)
702
        logging.info("added item: {}".format(item))
703
        # Refresh the document view
704
        self.display_document()
705
        # Set the new selection
706
        self.stringvar_item.set(item.uid)
707
708
    @_log
709
    def remove(self):
710
        """Remove the selected item from the document."""
711
        newSelection = ""
712
        for c_currUID in self.treeview_outline.selection():
713
            # Find the item which should be selected once the current selection is removed.
714
            for currNeighbourStrategy in (
715
                self.treeview_outline.next,
716
                self.treeview_outline.prev,
717
                self.treeview_outline.parent,
718
            ):
719
                newSelection = currNeighbourStrategy(c_currUID)
720
                if newSelection != "":
721
                    break
722
            # Remove the item
723
            item = self.tree.find_item(c_currUID)
724
            logging.info("removing item {}...".format(item))
725
            item = self.tree.remove_item(item)
726
            logging.info("removed item: {}".format(item))
727
            # Set the new selection
728
            self.stringvar_item.set(newSelection)
729
            # Refresh the document view
730
            self.display_document()
731
732
    @_log
733
    def link(self):
734
        """Add the specified link to the current item."""
735
        # Add the specified link to the list
736
        uid = self.stringvar_link.get()
737
        if uid:
738
            self.listbox_links.insert(tk.END, uid)
739
            self.stringvar_link.set("")
740
741
            # Update the current item
742
            self.update_item()
743
744
    @_log
745
    def unlink(self):
746
        """Remove the currently selected link from the current item."""
747
        # Remove the selected link from the list (if selected)
748
        index = self.listbox_links.curselection()
749
        if not index:
750
            return
751
        self.listbox_links.delete(index)
752
753
        # Update the current item
754
        self.update_item()
755
756
    @_log
757
    def followlink(self, uid):
758
        """Display a given uid."""
759
        # Update the current item
760
        self.ignore = False
761
        self.update_item()
762
763
        # Load the good document.
764
        document = self.tree.find_document(uid.prefix)
765
        index = list(self.tree).index(document)
766
        self.combobox_documents.current(index)
767
        self.display_document()
768
769
        # load the good Item
770
        self.stringvar_item.set(uid)
771
772
    def create_properties_widget(self, parent):
773
        frame = ttk.Frame(parent)
774
775
        frame.columnconfigure(0, weight=1)
776
        frame.rowconfigure(0, weight=1)
777
        frame.rowconfigure(1, weight=1)
778
        frame.rowconfigure(2, weight=1)
779
        frame.rowconfigure(3, weight=1)
780
        frame.rowconfigure(4, weight=1)
781
782
        widget.Label(frame, text="Properties:").grid(row=0, column=0, sticky=tk.NW)
783
        widget.Checkbutton(frame, text="Active", variable=self.intvar_active).grid(
784
            row=1, column=0, sticky=tk.NW
785
        )
786
        widget.Checkbutton(frame, text="Derived", variable=self.intvar_derived).grid(
787
            row=2, column=0, sticky=tk.NW
788
        )
789
        widget.Checkbutton(
790
            frame, text="Normative", variable=self.intvar_normative
791
        ).grid(row=3, column=0, sticky=tk.NW)
792
        widget.Checkbutton(frame, text="Heading", variable=self.intvar_heading).grid(
793
            row=4, column=0, sticky=tk.NW
794
        )
795
796
        return frame
797
798
    def create_links_widget(self, parent):
799
        frame = ttk.Frame(parent)
800
801
        frame.columnconfigure(0, weight=1)
802
        frame.columnconfigure(1, weight=1)
803
        frame.columnconfigure(2, weight=0)
804
        frame.columnconfigure(3, weight=0)
805
        frame.rowconfigure(0, weight=1)
806
        frame.rowconfigure(1, weight=1)
807
        frame.rowconfigure(2, weight=1)
808
809
        width_uid = 10
810
        widget.Label(frame, text="Links:").grid(
811
            row=0, column=0, columnspan=1, sticky=tk.NW
812
        )
813
        widget.Entry(frame, textvariable=self.stringvar_link).grid(
814
            row=1, column=0, columnspan=2, sticky=tk.EW + tk.N
815
        )
816
        widget.Button(frame, text="+", command=self.link).grid(
817
            row=1, column=2, columnspan=1, sticky=tk.EW + tk.N
818
        )
819
        widget.Button(frame, text="-", command=self.unlink).grid(
820
            row=1, column=3, columnspan=1, sticky=tk.EW + tk.N
821
        )
822
        self.listbox_links = widget.Listbox(frame, width=width_uid)
823
        self.listbox_links.grid(
824
            row=2,
825
            column=0,
826
            rowspan=2,
827
            columnspan=4,
828
            padx=(3, 0),
829
            pady=(3, 0),
830
            sticky=tk.NSEW,
831
        )
832
833
        return frame
834
835
    def create_reference_widget(self, parent):
836
        frame = ttk.Frame(parent)
837
838
        frame.columnconfigure(0, weight=1)
839
        frame.rowconfigure(0, weight=1)
840
        frame.rowconfigure(1, weight=1)
841
842
        widget.Label(frame, text="External Reference:").grid(
843
            row=0, column=0, sticky=tk.W
844
        )
845
        widget.Entry(frame, textvariable=self.stringvar_ref).grid(
846
            row=1, column=0, sticky=tk.NSEW
847
        )
848
849
        return frame
850