Passed
Pull Request — develop (#395)
by
unknown
01:50
created

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

Complexity

Conditions 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 11
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 2
nop 1
crap 6
1
#!/usr/bin/env python
2
# SPDX-License-Identifier: LGPL-3.0-only
0 ignored issues
show
coding-style introduced by
Too many lines in module (1037/1000)
Loading history...
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()  # no trace event
209
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.listbox_refs = None
221
        self.combobox_extended = None
222
        self.text_extendedvalue = None
223
        self.text_parents = None
224
        self.text_children = None
225
226
        # Initialize the GUI
227
        self.ignore = False  # flag to ignore internal events
228
        frame = self.init(root)
229
        frame.pack(fill=tk.BOTH, expand=1)
230
231
        # Start the application
232
        root.after(500, self.find)
233
234
    def init(self, root):
235
        """Initialize and return the main frame."""
236
        # pylint: disable=attribute-defined-outside-init
237
238
        # Shared arguments
239
        width_text = 30
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, style='hausstil.TFrame', **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(
340
                frame, style='hausstil.TFrame', columns=c_columnId
341
            )
342
            for col in c_columnId:
343
                self.treeview_outline.heading(col, text=col)
344
345
            # Add a Vertical scrollbar to the Treeview Outline
346
            treeview_outline_verticalScrollBar = widget.ScrollbarV(
347
                frame, command=self.treeview_outline.yview
348
            )
349
            treeview_outline_verticalScrollBar.grid(
350
                row=1, column=0, columnspan=1, **kw_gs
351
            )
352
            self.treeview_outline.configure(
353
                yscrollcommand=treeview_outline_verticalScrollBar.set
354
            )
355
356
            self.treeview_outline.bind(
357
                "<<TreeviewSelect>>", treeview_outline_treeviewselect
358
            )
359
            self.treeview_outline.bind("<Delete>", treeview_outline_delete)
360
            self.treeview_outline.grid(row=1, column=1, columnspan=3, **kw_gsp)
361
            self.text_items = widget.noUserInput_init(
362
                widget.Text(frame, width=width_text, wrap=tk.WORD)
363
            )
364
            self.text_items.grid(row=1, column=4, columnspan=2, **kw_gsp)
365
            self.text_items_hyperlink = utilTkinter.HyperlinkManager(self.text_items)
366
            widget.Button(frame, text="<", width=0, command=self.left).grid(
367
                row=2, column=0, sticky=tk.EW, padx=(2, 0)
368
            )
369
            widget.Button(frame, text="v", width=0, command=self.down).grid(
370
                row=2, column=1, sticky=tk.EW
371
            )
372
            widget.Button(frame, text="^", width=0, command=self.up).grid(
373
                row=2, column=2, sticky=tk.EW
374
            )
375
            widget.Button(frame, text=">", width=0, command=self.right).grid(
376
                row=2, column=3, sticky=tk.EW, padx=(0, 2)
377
            )
378
            widget.Button(frame, text="Add Item", command=self.add).grid(
379
                row=2, column=4, sticky=tk.W, **kw_gp
380
            )
381
            widget.Button(frame, text="Remove Selected Item", command=self.remove).grid(
382
                row=2, column=5, sticky=tk.E, **kw_gp
383
            )
384
385
            return frame
386
387
        def frame_item(root):
388
            """Frame for the currently selected item."""
389
            # Configure grid
390
            frame = ttk.Frame(root, **kw_f)
391
            frame.rowconfigure(0, weight=0)
392
            frame.rowconfigure(1, weight=4)
393
            frame.rowconfigure(2, weight=0)
394
            frame.rowconfigure(3, weight=1)
395
            frame.rowconfigure(4, weight=1)
396
            frame.rowconfigure(5, weight=1)
397
            frame.rowconfigure(6, weight=1)
398
            frame.rowconfigure(7, weight=0)
399
            frame.rowconfigure(8, weight=0)
400
            frame.rowconfigure(9, weight=0)
401
            frame.rowconfigure(10, weight=0)
402
            frame.rowconfigure(11, weight=4)
403
            frame.columnconfigure(0, weight=1, pad=kw_f['padding'] * 2)
404
            frame.columnconfigure(1, weight=1)
405
406
            @_log
407
            def text_focusin(_):
408
                """Handle entering a text field."""
409
                self.ignore = True
410
411
            @_log
412
            def text_item_focusout(event):
413
                """Handle updated text text."""
414
                self.ignore = False
415
                thewidget = event.widget
416
                value = thewidget.get('1.0', tk.END)
417
                self.stringvar_text.set(value)
418
419
            @_log
420
            def text_extendedvalue_focusout(event):
421
                """Handle updated extended attributes."""
422
                self.ignore = False
423
                thewidget = event.widget
424
                value = thewidget.get('1.0', tk.END)
425
                self.stringvar_extendedvalue.set(value)
426
427
            # Selected Item
428
            widget.Label(frame, text="Selected Item:").grid(
429
                row=0, column=0, columnspan=3, sticky=tk.W, **kw_gp
430
            )
431
            self.text_item = widget.Text(
432
                frame, width=width_text, height=height_text, wrap=tk.WORD
433
            )
434
            self.text_item.bind('<FocusIn>', text_focusin)
435
            self.text_item.bind('<FocusOut>', text_item_focusout)
436
            self.text_item.grid(row=1, column=0, columnspan=3, **kw_gsp)
437
438
            # Column: Properties
439
            self.create_properties_widget(frame).grid(
440
                row=2, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
441
            )
442
443
            # Column: Links
444
            self.create_links_widget(frame).grid(
445
                row=4, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
446
            )
447
448
            # External Reference
449
            self.create_reference_widget(frame).grid(
450
                row=6, rowspan=2, column=0, columnspan=2, sticky=tk.NSEW, **kw_gp
451
            )
452
453
            widget.Label(frame, text="Extended Attributes:").grid(
454
                row=8, column=0, columnspan=3, sticky=tk.W, **kw_gp
455
            )
456
            self.combobox_extended = widget.Combobox(
457
                frame, textvariable=self.stringvar_extendedkey
458
            )
459
            self.combobox_extended.grid(row=10, column=0, columnspan=3, **kw_gsp)
460
            self.text_extendedvalue = widget.Text(
461
                frame, width=width_text, height=height_ext, wrap=tk.WORD
462
            )
463
            self.text_extendedvalue.bind('<FocusIn>', text_focusin)
464
            self.text_extendedvalue.bind('<FocusOut>', text_extendedvalue_focusout)
465
            self.text_extendedvalue.grid(row=11, column=0, columnspan=3, **kw_gsp)
466
467
            return frame
468
469
        def frame_family(root):
470
            """Frame for the parent and child document items."""
471
            # Configure grid
472
            frame = ttk.Frame(root, **kw_f)
473
            frame.rowconfigure(0, weight=0)
474
            frame.rowconfigure(1, weight=1)
475
            frame.rowconfigure(2, weight=0)
476
            frame.rowconfigure(3, weight=1)
477
            frame.columnconfigure(0, weight=1)
478
479
            # Place widgets
480
            widget.Label(frame, text="Linked To:").grid(
481
                row=0, column=0, sticky=tk.W, **kw_gp
482
            )
483
            self.text_parents = widget.noUserInput_init(
484
                widget.Text(frame, width=width_text, wrap=tk.WORD)
485
            )
486
            self.text_parents_hyperlink = utilTkinter.HyperlinkManager(
487
                self.text_parents
488
            )
489
            self.text_parents.grid(row=1, column=0, **kw_gsp)
490
            widget.Label(frame, text="Linked From:").grid(
491
                row=2, column=0, sticky=tk.W, **kw_gp
492
            )
493
            self.text_children = widget.noUserInput_init(
494
                widget.Text(frame, width=width_text, wrap=tk.WORD)
495
            )
496
            self.text_children_hyperlink = utilTkinter.HyperlinkManager(
497
                self.text_children
498
            )
499
            self.text_children.grid(row=3, column=0, **kw_gsp)
500
501
            return frame
502
503
        # Place widgets
504
        frame_project(frame).grid(row=0, column=0, columnspan=2, **kw_gs)
505
        frame_tree(frame).grid(row=0, column=2, columnspan=2, **kw_gs)
506
        frame_document(frame).grid(row=1, column=0, **kw_gs)
507
        frame_item(frame).grid(row=1, column=1, columnspan=2, **kw_gs)
508
        frame_family(frame).grid(row=1, column=3, **kw_gs)
509
510
        return frame
511
512
    @_log
513
    def find(self):
514
        """Find the root of the project."""
515
        if not self.stringvar_project.get():
516
            try:
517
                path = vcs.find_root(self.cwd)
518
            except DoorstopError as exc:
519
                log.error(exc)
520
            else:
521
                self.stringvar_project.set(path)
522
523
    @_log
524
    def browse(self):
525
        """Browse for the root of a project."""
526
        path = filedialog.askdirectory()
527
        log.debug("path: {}".format(path))
528
        if path:
529
            self.stringvar_project.set(path)
530
531
    @_log
532
    def display_tree(self, *_):
533
        """Display the currently selected tree."""
534
        # Set the current tree
535
        self.tree = builder.build(root=self.stringvar_project.get())
536
        log.info("displaying tree...")
537
538
        # Display the documents in the tree
539
        values = [
540
            "{} ({})".format(document.prefix, document.relpath)
541
            for document in self.tree
542
        ]
543
        self.combobox_documents['values'] = values
544
545
        # Select the first document
546
        if len(self.tree):  # pylint: disable=len-as-condition
547
            self.combobox_documents.current(0)
548
        else:
549
            logging.warning("no documents to display")
550
551
    @_log
552
    def display_document(self, *_):
553
        """Display the currently selected document."""
554
        # Set the current document
555
        index = self.combobox_documents.current()
556
        self.document = list(self.tree)[index]
557
        log.info("displaying document {}...".format(self.document))
558
559
        # Record the currently opened items.
560
        c_openItem = []
561
        for c_currUID in utilTkinter.getAllChildren(self.treeview_outline):
562
            if self.treeview_outline.item(c_currUID)["open"]:
563
                c_openItem.append(c_currUID)
564
565
        # Record the currently selected items.
566
        c_selectedItem = self.treeview_outline.selection()
567
568
        # Clear the widgets
569
        self.treeview_outline.delete(*self.treeview_outline.get_children())
570
        widget.noUserInput_delete(self.text_items, '1.0', tk.END)
571
        self.text_items_hyperlink.reset()
572
573
        # Display the items in the document
574
        c_levelsItem = [""]
575
        for item in self.document.items:
576
            theParent = next(
577
                iter(reversed([x for x in c_levelsItem[: item.depth]])), ""
578
            )
579
580
            while len(c_levelsItem) < item.depth:
581
                c_levelsItem.append(item.uid)
582
            c_levelsItem = c_levelsItem[: item.depth]
583
            for x in range(item.depth):
584
                c_levelsItem.append(item.uid)
585
586
            # Add the item to the document outline
587
            self.treeview_outline.insert(
588
                theParent,
589
                tk.END,
590
                item.uid,
591
                text=item.level,
592
                values=(item.uid,),
593
                open=item.uid in c_openItem,
594
            )
595
596
            # Add the item to the document text
597
            widget.noUserInput_insert(
598
                self.text_items, tk.END, "{t}".format(t=item.text or item.ref or '???')
599
            )
600
            widget.noUserInput_insert(self.text_items, tk.END, " [")
601
            widget.noUserInput_insert(
602
                self.text_items,
603
                tk.END,
604
                item.uid,
605
                self.text_items_hyperlink.add(
606
                    # pylint: disable=unnecessary-lambda
607
                    lambda c_theURL: self.followlink(c_theURL),
608
                    item.uid,
609
                    ["refLink"],
610
                ),
611
            )
612
            widget.noUserInput_insert(self.text_items, tk.END, "]\n\n")
613
614
        # Set tree view selection
615
        c_selectedItem = [
616
            x
617
            for x in c_selectedItem
618
            if x in utilTkinter.getAllChildren(self.treeview_outline)
619
        ]
620
        if c_selectedItem:
621
            # Restore selection
622
            self.treeview_outline.selection_set(c_selectedItem)
623
        else:
624
            # Select the first item
625
            for uid in utilTkinter.getAllChildren(self.treeview_outline):
626
                self.stringvar_item.set(uid)
627
                break
628
            else:
629
                logging.warning("no items to display")
630
                self.stringvar_item.set("")
631
632
    @_log
633
    def display_item(self, *_):
634
        """Display the currently selected item."""
635
        try:
636
            self.ignore = True
637
638
            # Fetch the current item
639
            uid = self.stringvar_item.get()
640
            if uid == "":
641
                self.item = None
642
            else:
643
                try:
644
                    self.item = self.tree.find_item(uid)
645
                except DoorstopError:
646
                    pass
647
            log.info("displaying item {}...".format(self.item))
648
649
            if uid != "":
650
                if uid not in self.treeview_outline.selection():
651
                    self.treeview_outline.selection_set((uid,))
652
                self.treeview_outline.see(uid)
653
654
            # Display the item's text
655
            self.text_item.replace(
656
                '1.0', tk.END, "" if self.item is None else self.item.text
657
            )
658
659
            # Display the item's properties
660
            self.stringvar_text.set("" if self.item is None else self.item.text)
661
            self.intvar_active.set(False if self.item is None else self.item.active)
662
            self.intvar_derived.set(False if self.item is None else self.item.derived)
663
            self.intvar_normative.set(
664
                False if self.item is None else self.item.normative
665
            )
666
            self.intvar_heading.set(False if self.item is None else self.item.heading)
667
668
            # Display the item's links
669
            self.listbox_links.delete(0, tk.END)
670
            if self.item is not None:
671
                for uid in self.item.links:
672
                    self.listbox_links.insert(tk.END, uid)
673
            self.stringvar_link.set('')
674
675
            # Display the item's external reference
676
            self.listbox_refs.delete(0, tk.END)
677
            if self.item is not None and isinstance(self.item.ref, str) and len(self.item.ref) > 0:
0 ignored issues
show
Unused Code introduced by
Do not use len(SEQUENCE) to determine if a sequence is empty
Loading history...
678
                self.listbox_refs.insert(tk.END, self.item.ref)
679
            else:
680
                if self.item is not None and isinstance(self.item.ref, list):
681
                    for ref in self.item.ref:
682
                        self.listbox_refs.insert(tk.END, ref['path'])
683
            self.stringvar_ref.set('')
684
685
            # Display the item's extended attributes
686
            values = None if self.item is None else self.item.extended
687
            self.combobox_extended['values'] = values or ['']
688
            if self.item is not None:
689
                self.combobox_extended.current(0)
690
691
            # Display the items this item links to
692
            widget.noUserInput_delete(self.text_parents, '1.0', tk.END)
693
            self.text_parents_hyperlink.reset()
694
            if self.item is not None:
695
                for uid in self.item.links:
696
                    try:
697
                        item = self.tree.find_item(uid)
698
                    except DoorstopError:
699
                        text = "???"
700
                    else:
701
                        text = item.text or item.ref or '???'
702
                        uid = item.uid
703
704
                    widget.noUserInput_insert(
705
                        self.text_parents, tk.END, "{t}".format(t=text)
706
                    )
707
                    widget.noUserInput_insert(self.text_parents, tk.END, " [")
708
                    widget.noUserInput_insert(
709
                        self.text_parents,
710
                        tk.END,
711
                        uid,
712
                        self.text_parents_hyperlink.add(
713
                            # pylint: disable=unnecessary-lambda
714
                            lambda c_theURL: self.followlink(c_theURL),
715
                            uid,
716
                            ["refLink"],
717
                        ),
718
                    )
719
                    widget.noUserInput_insert(self.text_parents, tk.END, "]\n\n")
720
721
            # Display the items this item has links from
722
            widget.noUserInput_delete(self.text_children, '1.0', 'end')
723
            self.text_children_hyperlink.reset()
724
            if self.item is not None:
725
                for uid in self.item.find_child_links():
726
                    item = self.tree.find_item(uid)
727
                    text = item.text or item.ref or '???'
728
                    uid = item.uid
729
730
                    widget.noUserInput_insert(
731
                        self.text_children, tk.END, "{t}".format(t=text)
732
                    )
733
                    widget.noUserInput_insert(self.text_children, tk.END, " [")
734
                    widget.noUserInput_insert(
735
                        self.text_children,
736
                        tk.END,
737
                        uid,
738
                        self.text_children_hyperlink.add(
739
                            # pylint: disable=unnecessary-lambda
740
                            lambda c_theURL: self.followlink(c_theURL),
741
                            uid,
742
                            ["refLink"],
743
                        ),
744
                    )
745
                    widget.noUserInput_insert(self.text_children, tk.END, "]\n\n")
746
        finally:
747
            self.ignore = False
748
749
    @_log
750
    def display_extended(self, *_):
751
        """Display the currently selected extended attribute."""
752
        try:
753
            self.ignore = True
754
755
            name = self.stringvar_extendedkey.get()
756
            log.debug("displaying extended attribute '{}'...".format(name))
757
            self.text_extendedvalue.replace('1.0', tk.END, self.item.get(name, ""))
758
        finally:
759
            self.ignore = False
760
761
    @_log
762
    def update_item(self, *_):
763
        """Update the current item from the fields."""
764
        if self.ignore:
765
            return
766
        if not self.item:
767
            logging.warning("no item selected")
768
            return
769
770
        # Update the current item
771
        log.info("updating {}...".format(self.item))
772
        self.item.auto = False
773
        self.item.text = self.stringvar_text.get()
774
        self.item.active = self.intvar_active.get()
775
        self.item.derived = self.intvar_derived.get()
776
        self.item.normative = self.intvar_normative.get()
777
        self.item.heading = self.intvar_heading.get()
778
        self.item.links = self.listbox_links.get(0, tk.END)
779
780
        if isinstance(self.item.ref, str):
781
            if self.listbox_refs.size() == 1:
782
                only_ref = self.listbox_refs.get(0)
783
                self.item.ref = only_ref
784
            else:
785
                self.item.ref = ''
786
        else:
787
            listbox_refs = self.listbox_refs.get(0, tk.END)
788
            item_refs = []
789
            for listbox_ref in listbox_refs:
790
                item_refs.append({
791
                    'path': listbox_ref,
792
                    'type': 'file'
793
                })
794
            self.item.ref = item_refs
795
796
        name = self.stringvar_extendedkey.get()
797
        if name:
798
            self.item.set(name, self.stringvar_extendedvalue.get())
799
        self.item.save()
800
801
        # Re-select this item
802
        self.display_document()
803
804
    @_log
805
    def left(self):
806
        """Dedent the current item's level."""
807
        self.item.level <<= 1
808
        self.document.reorder(keep=self.item)
809
        self.display_document()
810
811
    @_log
812
    def down(self):
813
        """Increment the current item's level."""
814
        self.item.level += 1
815
        self.document.reorder(keep=self.item)
816
        self.display_document()
817
818
    @_log
819
    def up(self):
820
        """Decrement the current item's level."""
821
        self.item.level -= 1
822
        self.document.reorder(keep=self.item)
823
        self.display_document()
824
825
    @_log
826
    def right(self):
827
        """Indent the current item's level."""
828
        self.item.level >>= 1
829
        self.document.reorder(keep=self.item)
830
        self.display_document()
831
832
    @_log
833
    def add(self):
834
        """Add a new item to the document."""
835
        logging.info("adding item to {}...".format(self.document))
836
        if self.item:
837
            level = self.item.level + 1
838
        else:
839
            level = None
840
        item = self.document.add_item(level=level)
841
        logging.info("added item: {}".format(item))
842
        # Refresh the document view
843
        self.display_document()
844
        # Set the new selection
845
        self.stringvar_item.set(item.uid)
846
847
    @_log
848
    def remove(self):
849
        """Remove the selected item from the document."""
850
        newSelection = ""
851
        for c_currUID in self.treeview_outline.selection():
852
            # Find the item which should be selected once the current selection is removed.
853
            for currNeighbourStrategy in (
854
                self.treeview_outline.next,
855
                self.treeview_outline.prev,
856
                self.treeview_outline.parent,
857
            ):
858
                newSelection = currNeighbourStrategy(c_currUID)
859
                if newSelection != "":
860
                    break
861
            # Remove the item
862
            item = self.tree.find_item(c_currUID)
863
            logging.info("removing item {}...".format(item))
864
            item = self.tree.remove_item(item)
865
            logging.info("removed item: {}".format(item))
866
            # Set the new selection
867
            self.stringvar_item.set(newSelection)
868
            # Refresh the document view
869
            self.display_document()
870
871
    @_log
872
    def link(self):
873
        """Add the specified link to the current item."""
874
        # Add the specified link to the list
875
        uid = self.stringvar_link.get()
876
        if uid:
877
            self.listbox_links.insert(tk.END, uid)
878
            self.stringvar_link.set('')
879
880
            # Update the current item
881
            self.update_item()
882
883
    @_log
884
    def unlink(self):
885
        """Remove the currently selected link from the current item."""
886
        # Remove the selected link from the list (if selected)
887
        index = self.listbox_links.curselection()
888
        if not index:
889
            return
890
        self.listbox_links.delete(index)
891
892
        # Update the current item
893
        self.update_item()
894
895
    @_log
896
    def followlink(self, uid):
897
        """Display a given uid."""
898
        # Update the current item
899
        self.ignore = False
900
        self.update_item()
901
902
        # Load the good document.
903
        document = self.tree.find_document(uid.prefix)
904
        index = list(self.tree).index(document)
905
        self.combobox_documents.current(index)
906
        self.display_document()
907
908
        # load the good Item
909
        self.stringvar_item.set(uid)
910
911
    @_log
912
    def link_ref(self):
913
        """Add the specified ref to the current item."""
914
        # Add the specified ref to the list
915
        uid = self.stringvar_ref.get()
916
        if uid:
917
            self.listbox_refs.insert(tk.END, uid)
918
            self.stringvar_ref.set('')
919
920
            # Update the current item
921
            self.update_item()
922
923
    @_log
924
    def unlink_ref(self):
925
        """Remove the currently selected ref from the current item."""
926
        # Remove the selected ref from the list (if selected)
927
        index = self.listbox_refs.curselection()
928
        if not index:
929
            return
930
        self.listbox_refs.delete(index)
931
932
        # Update the current item
933
        self.update_item()
934
935
    def create_properties_widget(self, parent):
936
        frame = ttk.Frame(parent)
937
938
        frame.columnconfigure(0, weight=1)
939
        frame.rowconfigure(0, weight=1)
940
        frame.rowconfigure(1, weight=1)
941
        frame.rowconfigure(2, weight=1)
942
        frame.rowconfigure(3, weight=1)
943
        frame.rowconfigure(4, weight=1)
944
945
        widget.Label(frame, text="Properties:").grid(row=0, column=0, sticky=tk.NW, pady=(3, 3))
946
        widget.Checkbutton(frame, text="Active", variable=self.intvar_active).grid(
947
            row=1, column=0, sticky=tk.NW
948
        )
949
        widget.Checkbutton(frame, text="Derived", variable=self.intvar_derived).grid(
950
            row=2, column=0, sticky=tk.NW
951
        )
952
        widget.Checkbutton(
953
            frame, text="Normative", variable=self.intvar_normative
954
        ).grid(row=3, column=0, sticky=tk.NW)
955
        widget.Checkbutton(frame, text="Heading", variable=self.intvar_heading).grid(
956
            row=4, column=0, sticky=tk.NW
957
        )
958
959
        return frame
960
961 View Code Duplication
    def create_links_widget(self, parent):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
962
        frame = ttk.Frame(parent)
963
964
        frame.columnconfigure(0, weight=1)
965
        frame.columnconfigure(1, weight=1)
966
        frame.columnconfigure(2, weight=0)
967
        frame.columnconfigure(3, weight=0)
968
        frame.rowconfigure(0, weight=1)
969
        frame.rowconfigure(1, weight=1)
970
        frame.rowconfigure(2, weight=1)
971
972
        width_uid = 10
973
        widget.Label(frame, text="Links:").grid(
974
            row=0, column=0, columnspan=1, sticky=tk.NW
975
        )
976
        widget.Entry(frame, textvariable=self.stringvar_link).grid(
977
            row=1, column=0, columnspan=2, sticky=tk.EW + tk.N
978
        )
979
        widget.Button(frame, text="+", command=self.link).grid(
980
            row=1, column=2, columnspan=1, sticky=tk.EW + tk.N
981
        )
982
        widget.Button(frame, text="-", command=self.unlink).grid(
983
            row=1, column=3, columnspan=1, sticky=tk.EW + tk.N
984
        )
985
        self.listbox_links = widget.Listbox(frame, width=width_uid, height=5)
986
        self.listbox_links.grid(
987
            row=2,
988
            column=0,
989
            rowspan=2,
990
            columnspan=4,
991
            padx=(3, 0),
992
            pady=(3, 0),
993
            sticky=tk.NSEW,
994
        )
995
996
        return frame
997
998 View Code Duplication
    def create_reference_widget(self, parent):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
999
        frame = ttk.Frame(parent)
1000
1001
        frame.columnconfigure(0, weight=1)
1002
        frame.columnconfigure(1, weight=1)
1003
        frame.columnconfigure(2, weight=0)
1004
        frame.columnconfigure(3, weight=0)
1005
        frame.rowconfigure(0, weight=1)
1006
        frame.rowconfigure(1, weight=1)
1007
        frame.rowconfigure(2, weight=1)
1008
1009
        width_uid = 10
1010
        widget.Label(frame, text="External references:").grid(
1011
            row=0, column=0, columnspan=1, sticky=tk.NW
1012
        )
1013
        widget.Entry(frame, textvariable=self.stringvar_ref).grid(
1014
            row=1, column=0, columnspan=2, sticky=tk.EW + tk.N
1015
        )
1016
        widget.Button(frame, text="+", command=self.link_ref).grid(
1017
            row=1, column=2, columnspan=1, sticky=tk.EW + tk.N
1018
        )
1019
        widget.Button(frame, text="-", command=self.unlink_ref).grid(
1020
            row=1, column=3, columnspan=1, sticky=tk.EW + tk.N
1021
        )
1022
        self.listbox_refs = widget.Listbox(frame, width=width_uid, height=5)
1023
        self.listbox_refs.grid(
1024
            row=2,
1025
            column=0,
1026
            rowspan=2,
1027
            columnspan=4,
1028
            padx=(3, 0),
1029
            pady=(3, 0),
1030
            sticky=tk.NSEW,
1031
        )
1032
1033
        return frame
1034
1035
1036
if __name__ == '__main__':
1037
    sys.exit(main())
1038