Completed
Push — develop ( ded20e...574d21 )
by Jace
22s queued 10s
created

doorstop.gui.main   F

Complexity

Total Complexity 89

Size/Duplication

Total Lines 951
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 89
eloc 632
dl 0
loc 951
ccs 52
cts 52
cp 1
rs 1.968
c 0
b 0
f 0

4 Functions

Rating   Name   Duplication   Size   Complexity  
A _configure_logging() 0 20 3
B run() 0 64 8
A _log() 0 15 3
A main() 0 36 3

20 Methods

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

How to fix   Complexity   

Complexity

Complex classes like doorstop.gui.main 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 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
        height_text = 10
240
        height_ext = 5
241
242
        # Shared keyword arguments
243
        kw_f = {'padding': 5}  # constructor arguments for frames
244
        kw_gp = {'padx': 2, 'pady': 2}  # grid arguments for padded widgets
245
        kw_gs = {'sticky': tk.NSEW}  # grid arguments for sticky widgets
246
        kw_gsp = dict(
247
            chain(kw_gs.items(), kw_gp.items())
248
        )  # grid arguments for sticky padded widgets
249
250
        root.bind_all("<Control-minus>", lambda arg: widget.adjustFontSize(-1))
251
        root.bind_all("<Control-equal>", lambda arg: widget.adjustFontSize(1))
252
        root.bind_all("<Control-0>", lambda arg: widget.resetFontSize())
253
254
        # Configure grid
255
        frame = ttk.Frame(root, **kw_f)
256
        frame.rowconfigure(0, weight=0)
257
        frame.rowconfigure(1, weight=1)
258
        frame.columnconfigure(0, weight=2)
259
        frame.columnconfigure(1, weight=1)
260
        frame.columnconfigure(2, weight=1)
261
        frame.columnconfigure(3, weight=2)
262
263
        # Create widgets
264
        def frame_project(root):
265
            """Frame for the current project."""
266
            # Configure grid
267
            frame = ttk.Frame(root, **kw_f)
268
            frame.rowconfigure(0, weight=1)
269
            frame.columnconfigure(0, weight=0)
270
            frame.columnconfigure(1, weight=1)
271
272
            # Place widgets
273
            widget.Label(frame, text="Project:").grid(row=0, column=0, **kw_gp)
274
            widget.Entry(frame, textvariable=self.stringvar_project).grid(
275
                row=0, column=1, **kw_gsp
276
            )
277
278
            return frame
279
280
        def frame_tree(root):
281
            """Frame for the current document."""
282
            # Configure grid
283
            frame = ttk.Frame(root, **kw_f)
284
            frame.rowconfigure(0, weight=1)
285
            frame.columnconfigure(0, weight=0)
286
            frame.columnconfigure(1, weight=1)
287
288
            # Place widgets
289
            widget.Label(frame, text="Document:").grid(row=0, column=0, **kw_gp)
290
            self.combobox_documents = widget.Combobox(
291
                frame, textvariable=self.stringvar_document, state="readonly"
292
            )
293
            self.combobox_documents.grid(row=0, column=1, **kw_gsp)
294
295
            return frame
296
297
        def frame_document(root):
298
            """Frame for current document's outline and items."""
299
            # Configure grid
300
            frame = ttk.Frame(root, **kw_f)
301
            frame.rowconfigure(0, weight=0)
302
            frame.rowconfigure(1, weight=5)
303
            frame.rowconfigure(2, weight=0)
304
            frame.rowconfigure(3, weight=0)
305
            frame.columnconfigure(0, weight=0)
306
            frame.columnconfigure(1, weight=0)
307
            frame.columnconfigure(2, weight=0)
308
            frame.columnconfigure(3, weight=0)
309
            frame.columnconfigure(4, weight=1)
310
            frame.columnconfigure(5, weight=1)
311
312
            @_log
313
            def treeview_outline_treeviewselect(event):
314
                """Handle selecting an item in the tree view."""
315
                if self.ignore:
316
                    return
317
                thewidget = event.widget
318
                curselection = thewidget.selection()
319
                if curselection:
320
                    uid = curselection[0]
321
                    self.stringvar_item.set(uid)
322
323
            @_log
324
            def treeview_outline_delete(event):  # pylint: disable=W0613
325
                """Handle deleting an item in the tree view."""
326
                if self.ignore:
327
                    return
328
                self.remove()
329
330
            # Place widgets
331
            widget.Label(frame, text="Outline:").grid(
332
                row=0, column=0, columnspan=4, sticky=tk.W, **kw_gp
333
            )
334
            widget.Label(frame, text="Items:").grid(
335
                row=0, column=4, columnspan=2, sticky=tk.W, **kw_gp
336
            )
337
            c_columnId = ("Id",)
338
            self.treeview_outline = widget.TreeView(frame, columns=c_columnId)
339
            for col in c_columnId:
340
                self.treeview_outline.heading(col, text=col)
341
342
            # Add a Vertical scrollbar to the Treeview Outline
343
            treeview_outline_verticalScrollBar = widget.ScrollbarV(
344
                frame, command=self.treeview_outline.yview
345
            )
346
            treeview_outline_verticalScrollBar.grid(
347
                row=1, column=0, columnspan=1, **kw_gs
348
            )
349
            self.treeview_outline.configure(
350
                yscrollcommand=treeview_outline_verticalScrollBar.set
351
            )
352
353
            self.treeview_outline.bind(
354
                "<<TreeviewSelect>>", treeview_outline_treeviewselect
355
            )
356
            self.treeview_outline.bind("<Delete>", treeview_outline_delete)
357
            self.treeview_outline.grid(row=1, column=1, columnspan=3, **kw_gsp)
358
            self.text_items = widget.noUserInput_init(
359
                widget.Text(frame, width=width_text, wrap=tk.WORD)
360
            )
361
            self.text_items.grid(row=1, column=4, columnspan=2, **kw_gsp)
362
            self.text_items_hyperlink = utilTkinter.HyperlinkManager(self.text_items)
363
            widget.Button(frame, text="<", width=0, command=self.left).grid(
364
                row=2, column=0, sticky=tk.EW, padx=(2, 0)
365
            )
366
            widget.Button(frame, text="v", width=0, command=self.down).grid(
367
                row=2, column=1, sticky=tk.EW
368
            )
369
            widget.Button(frame, text="^", width=0, command=self.up).grid(
370
                row=2, column=2, sticky=tk.EW
371
            )
372
            widget.Button(frame, text=">", width=0, command=self.right).grid(
373
                row=2, column=3, sticky=tk.EW, padx=(0, 2)
374
            )
375
            widget.Button(frame, text="Add Item", command=self.add).grid(
376
                row=2, column=4, sticky=tk.W, **kw_gp
377
            )
378
            widget.Button(frame, text="Remove Selected Item", command=self.remove).grid(
379
                row=2, column=5, sticky=tk.E, **kw_gp
380
            )
381
382
            return frame
383
384
        def frame_item(root):
385
            """Frame for the currently selected item."""
386
            # Configure grid
387
            frame = ttk.Frame(root, **kw_f)
388
            frame.rowconfigure(0, weight=0)
389
            frame.rowconfigure(1, weight=4)
390
            frame.rowconfigure(2, weight=0)
391
            frame.rowconfigure(3, weight=1)
392
            frame.rowconfigure(4, weight=1)
393
            frame.rowconfigure(5, weight=1)
394
            frame.rowconfigure(6, weight=1)
395
            frame.rowconfigure(7, weight=0)
396
            frame.rowconfigure(8, weight=0)
397
            frame.rowconfigure(9, weight=0)
398
            frame.rowconfigure(10, weight=0)
399
            frame.rowconfigure(11, weight=4)
400
            frame.columnconfigure(0, weight=1, pad=kw_f['padding'] * 2)
401
            frame.columnconfigure(1, weight=1)
402
403
            @_log
404
            def text_focusin(_):
405
                """Handle entering a text field."""
406
                self.ignore = True
407
408
            @_log
409
            def text_item_focusout(event):
410
                """Handle updated text text."""
411
                self.ignore = False
412
                thewidget = event.widget
413
                value = thewidget.get('1.0', tk.END)
414
                self.stringvar_text.set(value)
415
416
            @_log
417
            def text_extendedvalue_focusout(event):
418
                """Handle updated extended attributes."""
419
                self.ignore = False
420
                thewidget = event.widget
421
                value = thewidget.get('1.0', tk.END)
422
                self.stringvar_extendedvalue.set(value)
423
424
            # Selected Item
425
            widget.Label(frame, text="Selected Item:").grid(
426
                row=0, column=0, columnspan=3, sticky=tk.W, **kw_gp
427
            )
428
            self.text_item = widget.Text(
429
                frame, width=width_text, height=height_text, wrap=tk.WORD
430
            )
431
            self.text_item.bind('<FocusIn>', text_focusin)
432
            self.text_item.bind('<FocusOut>', text_item_focusout)
433
            self.text_item.grid(row=1, column=0, columnspan=3, **kw_gsp)
434
435
            # Column: Properties
436
            self.create_properties_widget(frame).grid(
437
                row=2, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
438
            )
439
440
            # Column: Links
441
            self.create_links_widget(frame).grid(
442
                row=4, rowspan=3, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
443
            )
444
445
            # External Reference
446
            widget.Label(frame, text="External Reference:").grid(
447
                row=7, column=0, columnspan=3, sticky=tk.W, **kw_gp
448
            )
449
            widget.Entry(frame, width=width_text, textvariable=self.stringvar_ref).grid(
450
                row=8, column=0, columnspan=3, **kw_gsp
451
            )
452
            widget.Label(frame, text="Extended Attributes:").grid(
453
                row=9, column=0, columnspan=3, sticky=tk.W, **kw_gp
454
            )
455
            self.combobox_extended = widget.Combobox(
456
                frame, textvariable=self.stringvar_extendedkey
457
            )
458
            self.combobox_extended.grid(row=10, column=0, columnspan=3, **kw_gsp)
459
            self.text_extendedvalue = widget.Text(
460
                frame, width=width_text, height=height_ext, wrap=tk.WORD
461
            )
462
            self.text_extendedvalue.bind('<FocusIn>', text_focusin)
463
            self.text_extendedvalue.bind('<FocusOut>', text_extendedvalue_focusout)
464
            self.text_extendedvalue.grid(row=11, column=0, columnspan=3, **kw_gsp)
465
466
            return frame
467
468
        def frame_family(root):
469
            """Frame for the parent and child document items."""
470
            # Configure grid
471
            frame = ttk.Frame(root, **kw_f)
472
            frame.rowconfigure(0, weight=0)
473
            frame.rowconfigure(1, weight=1)
474
            frame.rowconfigure(2, weight=0)
475
            frame.rowconfigure(3, weight=1)
476
            frame.columnconfigure(0, weight=1)
477
478
            # Place widgets
479
            widget.Label(frame, text="Linked To:").grid(
480
                row=0, column=0, sticky=tk.W, **kw_gp
481
            )
482
            self.text_parents = widget.noUserInput_init(
483
                widget.Text(frame, width=width_text, wrap=tk.WORD)
484
            )
485
            self.text_parents_hyperlink = utilTkinter.HyperlinkManager(
486
                self.text_parents
487
            )
488
            self.text_parents.grid(row=1, column=0, **kw_gsp)
489
            widget.Label(frame, text="Linked From:").grid(
490
                row=2, column=0, sticky=tk.W, **kw_gp
491
            )
492
            self.text_children = widget.noUserInput_init(
493
                widget.Text(frame, width=width_text, wrap=tk.WORD)
494
            )
495
            self.text_children_hyperlink = utilTkinter.HyperlinkManager(
496
                self.text_children
497
            )
498
            self.text_children.grid(row=3, column=0, **kw_gsp)
499
500
            return frame
501
502
        # Place widgets
503
        frame_project(frame).grid(row=0, column=0, columnspan=2, **kw_gs)
504
        frame_tree(frame).grid(row=0, column=2, columnspan=2, **kw_gs)
505
        frame_document(frame).grid(row=1, column=0, **kw_gs)
506
        frame_item(frame).grid(row=1, column=1, columnspan=2, **kw_gs)
507
        frame_family(frame).grid(row=1, column=3, **kw_gs)
508
509
        return frame
510
511
    @_log
512
    def find(self):
513
        """Find the root of the project."""
514
        if not self.stringvar_project.get():
515
            try:
516
                path = vcs.find_root(self.cwd)
517
            except DoorstopError as exc:
518
                log.error(exc)
519
            else:
520
                self.stringvar_project.set(path)
521
522
    @_log
523
    def browse(self):
524
        """Browse for the root of a project."""
525
        path = filedialog.askdirectory()
526
        log.debug("path: {}".format(path))
527
        if path:
528
            self.stringvar_project.set(path)
529
530
    @_log
531
    def display_tree(self, *_):
532
        """Display the currently selected tree."""
533
        # Set the current tree
534
        self.tree = builder.build(root=self.stringvar_project.get())
535
        log.info("displaying tree...")
536
537
        # Display the documents in the tree
538
        values = [
539
            "{} ({})".format(document.prefix, document.relpath)
540
            for document in self.tree
541
        ]
542
        self.combobox_documents['values'] = values
543
544
        # Select the first document
545
        if len(self.tree):  # pylint: disable=len-as-condition
546
            self.combobox_documents.current(0)
547
        else:
548
            logging.warning("no documents to display")
549
550
    @_log
551
    def display_document(self, *_):
552
        """Display the currently selected document."""
553
        # Set the current document
554
        index = self.combobox_documents.current()
555
        self.document = list(self.tree)[index]
556
        log.info("displaying document {}...".format(self.document))
557
558
        # Record the currently opened items.
559
        c_openItem = []
560
        for c_currUID in utilTkinter.getAllChildren(self.treeview_outline):
561
            if self.treeview_outline.item(c_currUID)["open"]:
562
                c_openItem.append(c_currUID)
563
564
        # Record the currently selected items.
565
        c_selectedItem = self.treeview_outline.selection()
566
567
        # Clear the widgets
568
        self.treeview_outline.delete(*self.treeview_outline.get_children())
569
        widget.noUserInput_delete(self.text_items, '1.0', tk.END)
570
        self.text_items_hyperlink.reset()
571
572
        # Display the items in the document
573
        c_levelsItem = [""]
574
        for item in self.document.items:
575
            theParent = next(
576
                iter(reversed([x for x in c_levelsItem[: item.depth]])), ""
577
            )
578
579
            while len(c_levelsItem) < item.depth:
580
                c_levelsItem.append(item.uid)
581
            c_levelsItem = c_levelsItem[: item.depth]
582
            for x in range(item.depth):
583
                c_levelsItem.append(item.uid)
584
585
            # Add the item to the document outline
586
            self.treeview_outline.insert(
587
                theParent,
588
                tk.END,
589
                item.uid,
590
                text=item.level,
591
                values=(item.uid,),
592
                open=item.uid in c_openItem,
593
            )
594
595
            # Add the item to the document text
596
            widget.noUserInput_insert(
597
                self.text_items, tk.END, "{t}".format(t=item.text or item.ref or '???')
598
            )
599
            widget.noUserInput_insert(self.text_items, tk.END, " [")
600
            widget.noUserInput_insert(
601
                self.text_items,
602
                tk.END,
603
                item.uid,
604
                self.text_items_hyperlink.add(
605
                    # pylint: disable=unnecessary-lambda
606
                    lambda c_theURL: self.followlink(c_theURL),
607
                    item.uid,
608
                    ["refLink"],
609
                ),
610
            )
611
            widget.noUserInput_insert(self.text_items, tk.END, "]\n\n")
612
613
        # Set tree view selection
614
        c_selectedItem = [
615
            x
616
            for x in c_selectedItem
617
            if x in utilTkinter.getAllChildren(self.treeview_outline)
618
        ]
619
        if c_selectedItem:
620
            # Restore selection
621
            self.treeview_outline.selection_set(c_selectedItem)
622
        else:
623
            # Select the first item
624
            for uid in utilTkinter.getAllChildren(self.treeview_outline):
625
                self.stringvar_item.set(uid)
626
                break
627
            else:
628
                logging.warning("no items to display")
629
                self.stringvar_item.set("")
630
631
    @_log
632
    def display_item(self, *_):
633
        """Display the currently selected item."""
634
        try:
635
            self.ignore = True
636
637
            # Fetch the current item
638
            uid = self.stringvar_item.get()
639
            if uid == "":
640
                self.item = None
641
            else:
642
                try:
643
                    self.item = self.tree.find_item(uid)
644
                except DoorstopError:
645
                    pass
646
            log.info("displaying item {}...".format(self.item))
647
648
            if uid != "":
649
                if uid not in self.treeview_outline.selection():
650
                    self.treeview_outline.selection_set((uid,))
651
                self.treeview_outline.see(uid)
652
653
            # Display the item's text
654
            self.text_item.replace(
655
                '1.0', tk.END, "" if self.item is None else self.item.text
656
            )
657
658
            # Display the item's properties
659
            self.stringvar_text.set("" if self.item is None else self.item.text)
660
            self.intvar_active.set(False if self.item is None else self.item.active)
661
            self.intvar_derived.set(False if self.item is None else self.item.derived)
662
            self.intvar_normative.set(
663
                False if self.item is None else self.item.normative
664
            )
665
            self.intvar_heading.set(False if self.item is None else self.item.heading)
666
667
            # Display the item's links
668
            self.listbox_links.delete(0, tk.END)
669
            if self.item is not None:
670
                for uid in self.item.links:
671
                    self.listbox_links.insert(tk.END, uid)
672
            self.stringvar_link.set('')
673
674
            # Display the item's external reference
675
            self.stringvar_ref.set("" if self.item is None else self.item.ref)
676
677
            # Display the item's extended attributes
678
            values = None if self.item is None else self.item.extended
679
            self.combobox_extended['values'] = values or ['']
680
            if self.item is not None:
681
                self.combobox_extended.current(0)
682
683
            # Display the items this item links to
684
            widget.noUserInput_delete(self.text_parents, '1.0', tk.END)
685
            self.text_parents_hyperlink.reset()
686
            if self.item is not None:
687
                for uid in self.item.links:
688
                    try:
689
                        item = self.tree.find_item(uid)
690
                    except DoorstopError:
691
                        text = "???"
692
                    else:
693
                        text = item.text or item.ref or '???'
694
                        uid = item.uid
695
696
                    widget.noUserInput_insert(
697
                        self.text_parents, tk.END, "{t}".format(t=text)
698
                    )
699
                    widget.noUserInput_insert(self.text_parents, tk.END, " [")
700
                    widget.noUserInput_insert(
701
                        self.text_parents,
702
                        tk.END,
703
                        uid,
704
                        self.text_parents_hyperlink.add(
705
                            # pylint: disable=unnecessary-lambda
706
                            lambda c_theURL: self.followlink(c_theURL),
707
                            uid,
708
                            ["refLink"],
709
                        ),
710
                    )
711
                    widget.noUserInput_insert(self.text_parents, tk.END, "]\n\n")
712
713
            # Display the items this item has links from
714
            widget.noUserInput_delete(self.text_children, '1.0', 'end')
715
            self.text_children_hyperlink.reset()
716
            if self.item is not None:
717
                for uid in self.item.find_child_links():
718
                    item = self.tree.find_item(uid)
719
                    text = item.text or item.ref or '???'
720
                    uid = item.uid
721
722
                    widget.noUserInput_insert(
723
                        self.text_children, tk.END, "{t}".format(t=text)
724
                    )
725
                    widget.noUserInput_insert(self.text_children, tk.END, " [")
726
                    widget.noUserInput_insert(
727
                        self.text_children,
728
                        tk.END,
729
                        uid,
730
                        self.text_children_hyperlink.add(
731
                            # pylint: disable=unnecessary-lambda
732
                            lambda c_theURL: self.followlink(c_theURL),
733
                            uid,
734
                            ["refLink"],
735
                        ),
736
                    )
737
                    widget.noUserInput_insert(self.text_children, tk.END, "]\n\n")
738
        finally:
739
            self.ignore = False
740
741
    @_log
742
    def display_extended(self, *_):
743
        """Display the currently selected extended attribute."""
744
        try:
745
            self.ignore = True
746
747
            name = self.stringvar_extendedkey.get()
748
            log.debug("displaying extended attribute '{}'...".format(name))
749
            self.text_extendedvalue.replace('1.0', tk.END, self.item.get(name, ""))
750
        finally:
751
            self.ignore = False
752
753
    @_log
754
    def update_item(self, *_):
755
        """Update the current item from the fields."""
756
        if self.ignore:
757
            return
758
        if not self.item:
759
            logging.warning("no item selected")
760
            return
761
762
        # Update the current item
763
        log.info("updating {}...".format(self.item))
764
        self.item.auto = False
765
        self.item.text = self.stringvar_text.get()
766
        self.item.active = self.intvar_active.get()
767
        self.item.derived = self.intvar_derived.get()
768
        self.item.normative = self.intvar_normative.get()
769
        self.item.heading = self.intvar_heading.get()
770
        self.item.links = self.listbox_links.get(0, tk.END)
771
        self.item.ref = self.stringvar_ref.get()
772
        name = self.stringvar_extendedkey.get()
773
        if name:
774
            self.item.set(name, self.stringvar_extendedvalue.get())
775
        self.item.save()
776
777
        # Re-select this item
778
        self.display_document()
779
780
    @_log
781
    def left(self):
782
        """Dedent the current item's level."""
783
        self.item.level <<= 1
784
        self.document.reorder(keep=self.item)
785
        self.display_document()
786
787
    @_log
788
    def down(self):
789
        """Increment the current item's level."""
790
        self.item.level += 1
791
        self.document.reorder(keep=self.item)
792
        self.display_document()
793
794
    @_log
795
    def up(self):
796
        """Decrement the current item's level."""
797
        self.item.level -= 1
798
        self.document.reorder(keep=self.item)
799
        self.display_document()
800
801
    @_log
802
    def right(self):
803
        """Indent the current item's level."""
804
        self.item.level >>= 1
805
        self.document.reorder(keep=self.item)
806
        self.display_document()
807
808
    @_log
809
    def add(self):
810
        """Add a new item to the document."""
811
        logging.info("adding item to {}...".format(self.document))
812
        if self.item:
813
            level = self.item.level + 1
814
        else:
815
            level = None
816
        item = self.document.add_item(level=level)
817
        logging.info("added item: {}".format(item))
818
        # Refresh the document view
819
        self.display_document()
820
        # Set the new selection
821
        self.stringvar_item.set(item.uid)
822
823
    @_log
824
    def remove(self):
825
        """Remove the selected item from the document."""
826
        newSelection = ""
827
        for c_currUID in self.treeview_outline.selection():
828
            # Find the item which should be selected once the current selection is removed.
829
            for currNeighbourStrategy in (
830
                self.treeview_outline.next,
831
                self.treeview_outline.prev,
832
                self.treeview_outline.parent,
833
            ):
834
                newSelection = currNeighbourStrategy(c_currUID)
835
                if newSelection != "":
836
                    break
837
            # Remove the item
838
            item = self.tree.find_item(c_currUID)
839
            logging.info("removing item {}...".format(item))
840
            item = self.tree.remove_item(item)
841
            logging.info("removed item: {}".format(item))
842
            # Set the new selection
843
            self.stringvar_item.set(newSelection)
844
            # Refresh the document view
845
            self.display_document()
846
847
    @_log
848
    def link(self):
849
        """Add the specified link to the current item."""
850
        # Add the specified link to the list
851
        uid = self.stringvar_link.get()
852
        if uid:
853
            self.listbox_links.insert(tk.END, uid)
854
            self.stringvar_link.set('')
855
856
            # Update the current item
857
            self.update_item()
858
859
    @_log
860
    def unlink(self):
861
        """Remove the currently selected link from the current item."""
862
        # Remove the selected link from the list
863
        index = self.listbox_links.curselection()
864
        self.listbox_links.delete(index)
865
866
        # Update the current item
867
        self.update_item()
868
869
    @_log
870
    def followlink(self, uid):
871
        """Display a given uid."""
872
        # Update the current item
873
        self.ignore = False
874
        self.update_item()
875
876
        # Load the good document.
877
        document = self.tree.find_document(uid.prefix)
878
        index = list(self.tree).index(document)
879
        self.combobox_documents.current(index)
880
        self.display_document()
881
882
        # load the good Item
883
        self.stringvar_item.set(uid)
884
885
    def create_properties_widget(self, parent):
886
        frame = ttk.Frame(parent)
887
888
        frame.columnconfigure(0, weight=1)
889
        frame.rowconfigure(0, weight=1)
890
        frame.rowconfigure(1, weight=1)
891
        frame.rowconfigure(2, weight=1)
892
        frame.rowconfigure(3, weight=1)
893
        frame.rowconfigure(4, weight=1)
894
895
        widget.Label(frame, text="Properties:").grid(row=0, column=0, sticky=tk.NW)
896
        widget.Checkbutton(frame, text="Active", variable=self.intvar_active).grid(
897
            row=1, column=0, sticky=tk.NW
898
        )
899
        widget.Checkbutton(frame, text="Derived", variable=self.intvar_derived).grid(
900
            row=2, column=0, sticky=tk.NW
901
        )
902
        widget.Checkbutton(
903
            frame, text="Normative", variable=self.intvar_normative
904
        ).grid(row=3, column=0, sticky=tk.NW)
905
        widget.Checkbutton(frame, text="Heading", variable=self.intvar_heading).grid(
906
            row=4, column=0, sticky=tk.NW
907
        )
908
909
        return frame
910
911
    def create_links_widget(self, parent):
912
        frame = ttk.Frame(parent)
913
914
        frame.columnconfigure(0, weight=1)
915
        frame.columnconfigure(1, weight=1)
916
        frame.columnconfigure(2, weight=0)
917
        frame.columnconfigure(3, weight=0)
918
        frame.rowconfigure(0, weight=1)
919
        frame.rowconfigure(1, weight=1)
920
        frame.rowconfigure(2, weight=1)
921
922
        width_uid = 10
923
        widget.Label(frame, text="Links:").grid(
924
            row=0, column=0, columnspan=1, sticky=tk.NW
925
        )
926
        widget.Entry(frame, textvariable=self.stringvar_link).grid(
927
            row=1, column=0, columnspan=2, sticky=tk.EW + tk.N
928
        )
929
        widget.Button(frame, text="+", command=self.link).grid(
930
            row=1, column=2, columnspan=1, sticky=tk.EW + tk.N
931
        )
932
        widget.Button(frame, text="-", command=self.unlink).grid(
933
            row=1, column=3, columnspan=1, sticky=tk.EW + tk.N
934
        )
935
        self.listbox_links = widget.Listbox(frame, width=width_uid)
936
        self.listbox_links.grid(
937
            row=2,
938
            column=0,
939
            rowspan=2,
940
            columnspan=4,
941
            padx=(3, 0),
942
            pady=(3, 0),
943
            sticky=tk.NSEW,
944
        )
945
946
        return frame
947
948
949
if __name__ == '__main__':
950
    sys.exit(main())
951