Passed
Push — develop ( 78e905...1af55c )
by Jace
02:16 queued 10s
created

Application.create_properties_widget()   A

Complexity

Conditions 1

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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